Auto-indexing service and GraphQL API for AT Protocol Records
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add user-agent header to all outbound HTTP requests

Add centralized http_client module that wraps httpc and hackney send
functions to automatically include 'user-agent: quickslice' header.
Updated all HTTP call sites to use the new module.

+407 -20
+341
dev-docs/plans/2025-12-22-user-agent-header.md
··· 1 + # User-Agent Header Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add a `user-agent: quickslice` header to all outbound HTTP requests via a centralized helper module. 6 + 7 + **Architecture:** Create `src/lib/http_client.gleam` that wraps `httpc.send` and `hackney.send`, automatically adding the User-Agent header before delegating. Update all call sites to use the new module. 8 + 9 + **Tech Stack:** Gleam, gleam_httpc, gleam_hackney 10 + 11 + --- 12 + 13 + ### Task 1: Create the HTTP Client Helper Module 14 + 15 + **Files:** 16 + - Create: `src/lib/http_client.gleam` 17 + 18 + **Step 1: Create the module with send function** 19 + 20 + ```gleam 21 + import gleam/bytes_builder.{type BytesBuilder} 22 + import gleam/hackney 23 + import gleam/http/request.{type Request} 24 + import gleam/http/response.{type Response} 25 + import gleam/httpc 26 + 27 + const user_agent = "quickslice" 28 + 29 + /// Send an HTTP request with the quickslice user-agent header. 30 + /// Use this instead of httpc.send directly. 31 + pub fn send( 32 + req: Request(String), 33 + ) -> Result(Response(String), httpc.HttpcError) { 34 + req 35 + |> request.set_header("user-agent", user_agent) 36 + |> httpc.send 37 + } 38 + 39 + /// Send an HTTP request with binary body using hackney. 40 + /// Use this instead of hackney.send directly. 41 + pub fn send_bits( 42 + req: Request(BytesBuilder), 43 + ) -> Result(Response(BitArray), hackney.Error) { 44 + req 45 + |> request.set_header("user-agent", user_agent) 46 + |> hackney.send 47 + } 48 + ``` 49 + 50 + **Step 2: Verify it compiles** 51 + 52 + Run: `gleam build` 53 + Expected: Build succeeds with no errors 54 + 55 + **Step 3: Commit** 56 + 57 + ```bash 58 + git add src/lib/http_client.gleam 59 + git commit -m "feat: add http_client module with user-agent header" 60 + ``` 61 + 62 + --- 63 + 64 + ### Task 2: Update did_resolver.gleam 65 + 66 + **Files:** 67 + - Modify: `src/lib/oauth/atproto/did_resolver.gleam` 68 + 69 + **Step 1: Update import** 70 + 71 + Replace: 72 + ```gleam 73 + import gleam/httpc 74 + ``` 75 + 76 + With: 77 + ```gleam 78 + import lib/http_client 79 + ``` 80 + 81 + **Step 2: Update send calls** 82 + 83 + Replace all occurrences of: 84 + ```gleam 85 + httpc.send(req) 86 + ``` 87 + 88 + With: 89 + ```gleam 90 + http_client.send(req) 91 + ``` 92 + 93 + **Step 3: Verify it compiles** 94 + 95 + Run: `gleam build` 96 + Expected: Build succeeds with no errors 97 + 98 + **Step 4: Commit** 99 + 100 + ```bash 101 + git add src/lib/oauth/atproto/did_resolver.gleam 102 + git commit -m "refactor: use http_client in did_resolver" 103 + ``` 104 + 105 + --- 106 + 107 + ### Task 3: Update bridge.gleam 108 + 109 + **Files:** 110 + - Modify: `src/lib/oauth/atproto/bridge.gleam` 111 + 112 + **Step 1: Update import** 113 + 114 + Replace: 115 + ```gleam 116 + import gleam/httpc 117 + ``` 118 + 119 + With: 120 + ```gleam 121 + import lib/http_client 122 + ``` 123 + 124 + **Step 2: Update send calls** 125 + 126 + Replace all occurrences of: 127 + ```gleam 128 + httpc.send(req) 129 + ``` 130 + 131 + With: 132 + ```gleam 133 + http_client.send(req) 134 + ``` 135 + 136 + **Step 3: Verify it compiles** 137 + 138 + Run: `gleam build` 139 + Expected: Build succeeds with no errors 140 + 141 + **Step 4: Commit** 142 + 143 + ```bash 144 + git add src/lib/oauth/atproto/bridge.gleam 145 + git commit -m "refactor: use http_client in bridge" 146 + ``` 147 + 148 + --- 149 + 150 + ### Task 4: Update dpop.gleam 151 + 152 + **Files:** 153 + - Modify: `src/dpop.gleam` 154 + 155 + **Step 1: Update import** 156 + 157 + Replace: 158 + ```gleam 159 + import gleam/httpc 160 + ``` 161 + 162 + With: 163 + ```gleam 164 + import lib/http_client 165 + ``` 166 + 167 + **Step 2: Update send calls** 168 + 169 + Replace all occurrences of: 170 + ```gleam 171 + httpc.send(req) 172 + ``` 173 + 174 + With: 175 + ```gleam 176 + http_client.send(req) 177 + ``` 178 + 179 + **Step 3: Verify it compiles** 180 + 181 + Run: `gleam build` 182 + Expected: Build succeeds with no errors 183 + 184 + **Step 4: Commit** 185 + 186 + ```bash 187 + git add src/dpop.gleam 188 + git commit -m "refactor: use http_client in dpop" 189 + ``` 190 + 191 + --- 192 + 193 + ### Task 5: Update backfill.gleam 194 + 195 + **Files:** 196 + - Modify: `src/backfill.gleam` 197 + 198 + **Step 1: Update import** 199 + 200 + Replace: 201 + ```gleam 202 + import gleam/hackney 203 + ``` 204 + 205 + With: 206 + ```gleam 207 + import lib/http_client 208 + ``` 209 + 210 + Note: Keep any other hackney imports if they're used for types. 211 + 212 + **Step 2: Update send_bits calls** 213 + 214 + In the `send_bits_with_permit` function (or similar), replace: 215 + ```gleam 216 + hackney.send(req) 217 + ``` 218 + 219 + With: 220 + ```gleam 221 + http_client.send_bits(req) 222 + ``` 223 + 224 + **Step 3: Verify it compiles** 225 + 226 + Run: `gleam build` 227 + Expected: Build succeeds with no errors 228 + 229 + **Step 4: Commit** 230 + 231 + ```bash 232 + git add src/backfill.gleam 233 + git commit -m "refactor: use http_client in backfill" 234 + ``` 235 + 236 + --- 237 + 238 + ### Task 6: Update authorize.gleam 239 + 240 + **Files:** 241 + - Modify: `src/handlers/oauth/authorize.gleam` 242 + 243 + **Step 1: Update import** 244 + 245 + Replace: 246 + ```gleam 247 + import gleam/httpc 248 + ``` 249 + 250 + With: 251 + ```gleam 252 + import lib/http_client 253 + ``` 254 + 255 + **Step 2: Update send calls** 256 + 257 + Replace all occurrences of: 258 + ```gleam 259 + httpc.send(req) 260 + ``` 261 + 262 + With: 263 + ```gleam 264 + http_client.send(req) 265 + ``` 266 + 267 + **Step 3: Verify it compiles** 268 + 269 + Run: `gleam build` 270 + Expected: Build succeeds with no errors 271 + 272 + **Step 4: Commit** 273 + 274 + ```bash 275 + git add src/handlers/oauth/authorize.gleam 276 + git commit -m "refactor: use http_client in oauth authorize handler" 277 + ``` 278 + 279 + --- 280 + 281 + ### Task 7: Update admin_oauth_authorize.gleam 282 + 283 + **Files:** 284 + - Modify: `src/handlers/admin_oauth_authorize.gleam` 285 + 286 + **Step 1: Update import** 287 + 288 + Replace: 289 + ```gleam 290 + import gleam/httpc 291 + ``` 292 + 293 + With: 294 + ```gleam 295 + import lib/http_client 296 + ``` 297 + 298 + **Step 2: Update send calls** 299 + 300 + Replace all occurrences of: 301 + ```gleam 302 + httpc.send(req) 303 + ``` 304 + 305 + With: 306 + ```gleam 307 + http_client.send(req) 308 + ``` 309 + 310 + **Step 3: Verify it compiles** 311 + 312 + Run: `gleam build` 313 + Expected: Build succeeds with no errors 314 + 315 + **Step 4: Commit** 316 + 317 + ```bash 318 + git add src/handlers/admin_oauth_authorize.gleam 319 + git commit -m "refactor: use http_client in admin oauth handler" 320 + ``` 321 + 322 + --- 323 + 324 + ### Task 8: Final Verification 325 + 326 + **Step 1: Clean build** 327 + 328 + Run: `gleam clean && gleam build` 329 + Expected: Build succeeds with no errors or warnings 330 + 331 + **Step 2: Run tests** 332 + 333 + Run: `gleam test` 334 + Expected: All tests pass 335 + 336 + **Step 3: Final commit (if any cleanup needed)** 337 + 338 + ```bash 339 + git add -A 340 + git commit -m "chore: user-agent header implementation complete" 341 + ```
+3 -2
server/src/backfill.gleam
··· 25 25 import gleam/time/timestamp 26 26 import honk 27 27 import honk/errors 28 + import lib/http_client 28 29 import lib/oauth/did_cache 29 30 import logging 30 31 ··· 149 150 req: request.Request(String), 150 151 ) -> Result(gleam_http_response.Response(String), hackney.Error) { 151 152 acquire_permit() 152 - let result = hackney.send(req) 153 + let result = http_client.hackney_send(req) 153 154 release_permit() 154 155 result 155 156 } ··· 160 161 req: request.Request(bytes_tree.BytesTree), 161 162 ) -> Result(gleam_http_response.Response(BitArray), hackney.Error) { 162 163 acquire_permit() 163 - let result = hackney.send_bits(req) 164 + let result = http_client.hackney_send_bits(req) 164 165 release_permit() 165 166 result 166 167 }
+3 -3
server/src/dpop.gleam
··· 3 3 import gleam/http.{type Method, Delete, Get, Head, Options, Patch, Post, Put} 4 4 import gleam/http/request 5 5 import gleam/http/response.{type Response} 6 - import gleam/httpc 7 6 import gleam/list 8 7 import gleam/option.{None, Some} 9 8 import gleam/string 10 9 import jose_wrapper 10 + import lib/http_client 11 11 12 12 /// Make an authenticated DPoP request to a PDS with nonce retry support 13 13 /// ··· 89 89 |> request.set_header("content-type", "application/json") 90 90 |> request.set_body(body) 91 91 92 - case httpc.send(req) { 92 + case http_client.send(req) { 93 93 Error(_) -> Error("Request failed") 94 94 Ok(resp) -> Ok(resp) 95 95 } ··· 199 199 |> request.set_header("content-type", content_type) 200 200 |> request.set_body(body) 201 201 202 - case httpc.send_bits(req) { 202 + case http_client.send_bits(req) { 203 203 Error(_) -> Error("Request failed") 204 204 Ok(resp) -> { 205 205 // Convert BitArray response body to String
+3 -3
server/src/handlers/admin_oauth_authorize.gleam
··· 9 9 import gleam/erlang/process.{type Subject} 10 10 import gleam/http 11 11 import gleam/http/request as http_request 12 - import gleam/httpc 13 12 import gleam/json 14 13 import gleam/list 15 14 import gleam/option.{type Option, None, Some} 16 15 import gleam/string 17 16 import gleam/uri 17 + import lib/http_client 18 18 import lib/oauth/atproto/did_resolver 19 19 import lib/oauth/did_cache 20 20 import lib/oauth/dpop/keygen ··· 214 214 case http_request.to(pr_url) { 215 215 Error(_) -> Error("Invalid URL") 216 216 Ok(pr_req) -> { 217 - case httpc.send(pr_req) { 217 + case http_client.send(pr_req) { 218 218 Error(_) -> Error("Request failed") 219 219 Ok(pr_resp) -> { 220 220 case pr_resp.status { ··· 229 229 case http_request.to(as_url) { 230 230 Error(_) -> Error("Invalid auth server URL") 231 231 Ok(as_req) -> { 232 - case httpc.send(as_req) { 232 + case http_client.send(as_req) { 233 233 Error(_) -> Error("Auth server request failed") 234 234 Ok(as_resp) -> { 235 235 case as_resp.status {
+3 -3
server/src/handlers/oauth/authorize.gleam
··· 12 12 import gleam/erlang/process.{type Subject} 13 13 import gleam/http 14 14 import gleam/http/request as http_request 15 - import gleam/httpc 16 15 import gleam/json 17 16 import gleam/list 18 17 import gleam/option.{type Option, None, Some} 19 18 import gleam/result 20 19 import gleam/string 21 20 import gleam/uri 21 + import lib/http_client 22 22 import lib/oauth/atproto/did_resolver 23 23 import lib/oauth/did_cache 24 24 import lib/oauth/dpop/keygen ··· 457 457 ) 458 458 459 459 use pr_resp <- result.try( 460 - httpc.send(pr_req) 460 + http_client.send(pr_req) 461 461 |> result.map_error(fn(_) { "Request failed" }), 462 462 ) 463 463 ··· 482 482 ) 483 483 484 484 use as_resp <- result.try( 485 - httpc.send(as_req) 485 + http_client.send(as_req) 486 486 |> result.map_error(fn(_) { "Request failed" }), 487 487 ) 488 488
+45
server/src/lib/http_client.gleam
··· 1 + import gleam/bytes_tree.{type BytesTree} 2 + import gleam/hackney 3 + import gleam/http/request.{type Request} 4 + import gleam/http/response.{type Response} 5 + import gleam/httpc 6 + 7 + const user_agent = "quickslice" 8 + 9 + /// Send an HTTP request with the quickslice user-agent header. 10 + /// Use this instead of httpc.send directly. 11 + pub fn send(req: Request(String)) -> Result(Response(String), httpc.HttpError) { 12 + req 13 + |> request.set_header("user-agent", user_agent) 14 + |> httpc.send 15 + } 16 + 17 + /// Send an HTTP request with binary body using httpc. 18 + /// Use this instead of httpc.send_bits directly. 19 + pub fn send_bits( 20 + req: Request(BitArray), 21 + ) -> Result(Response(BitArray), httpc.HttpError) { 22 + req 23 + |> request.set_header("user-agent", user_agent) 24 + |> httpc.send_bits 25 + } 26 + 27 + /// Send an HTTP request using hackney. 28 + /// Use this instead of hackney.send directly. 29 + pub fn hackney_send( 30 + req: Request(String), 31 + ) -> Result(Response(String), hackney.Error) { 32 + req 33 + |> request.set_header("user-agent", user_agent) 34 + |> hackney.send 35 + } 36 + 37 + /// Send an HTTP request with binary body using hackney. 38 + /// Use this instead of hackney.send_bits directly. 39 + pub fn hackney_send_bits( 40 + req: Request(BytesTree), 41 + ) -> Result(Response(BitArray), hackney.Error) { 42 + req 43 + |> request.set_header("user-agent", user_agent) 44 + |> hackney.send_bits 45 + }
+5 -5
server/src/lib/oauth/atproto/bridge.gleam
··· 7 7 import gleam/erlang/process 8 8 import gleam/http 9 9 import gleam/http/request 10 - import gleam/httpc 11 10 import gleam/int 12 11 import gleam/json 13 12 import gleam/list ··· 15 14 import gleam/result 16 15 import gleam/string 17 16 import gleam/uri 17 + import lib/http_client 18 18 import lib/oauth/atproto/did_resolver 19 19 import lib/oauth/atproto/types as atp_types 20 20 import lib/oauth/crypto/jwt ··· 242 242 ) 243 243 244 244 use resp <- result.try( 245 - httpc.send(req) 245 + http_client.send(req) 246 246 |> result.map_error(fn(_) { 247 247 HTTPError("Failed to fetch protected resource metadata") 248 248 }), ··· 292 292 ) 293 293 294 294 use resp <- result.try( 295 - httpc.send(req) 295 + http_client.send(req) 296 296 |> result.map_error(fn(_) { 297 297 HTTPError("Failed to fetch authorization server metadata") 298 298 }), ··· 422 422 |> request.set_body(body_with_assertion) 423 423 424 424 use resp <- result.try( 425 - httpc.send(req) 425 + http_client.send(req) 426 426 |> result.map_error(fn(_) { HTTPError("Failed to send token request") }), 427 427 ) 428 428 ··· 499 499 |> request.set_body(body) 500 500 501 501 use resp <- result.try( 502 - httpc.send(req) 502 + http_client.send(req) 503 503 |> result.map_error(fn(_) { HTTPError("Failed to send token request") }), 504 504 ) 505 505
+4 -4
server/src/lib/oauth/atproto/did_resolver.gleam
··· 2 2 import gleam/dynamic/decode 3 3 import gleam/erlang/process.{type Subject} 4 4 import gleam/http/request 5 - import gleam/httpc 6 5 import gleam/json 7 6 import gleam/list 8 7 import gleam/option.{type Option, None} 9 8 import gleam/result 10 9 import gleam/string 10 + import lib/http_client 11 11 import lib/oauth/atproto/types.{type ATProtoError} 12 12 import lib/oauth/did_cache 13 13 ··· 82 82 ) 83 83 84 84 use resp <- result.try( 85 - httpc.send(req) 85 + http_client.send(req) 86 86 |> result.map_error(fn(_) { types.HTTPError("Request failed") }), 87 87 ) 88 88 ··· 108 108 ) 109 109 110 110 use resp <- result.try( 111 - httpc.send(req) 111 + http_client.send(req) 112 112 |> result.map_error(fn(_) { types.HTTPError("Request failed") }), 113 113 ) 114 114 ··· 134 134 ) 135 135 136 136 use resp <- result.try( 137 - httpc.send(req) 137 + http_client.send(req) 138 138 |> result.map_error(fn(_) { types.HTTPError("Request failed") }), 139 139 ) 140 140