See the best posts from any Bluesky account
0
fork

Configure Feed

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

Send a favs.blue User-Agent on AppView requests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+38 -2
+18 -2
app/lib/atproto/client.ts
··· 508 508 } 509 509 510 510 /** 511 + * User-Agent sent on every AppView request so Bluesky ops can identify our 512 + * traffic and contact us if we're misbehaving. 513 + */ 514 + export const ATPROTO_USER_AGENT = 'favs.blue (+https://favs.blue)' 515 + 516 + /** 511 517 * Create a production AtprotoClient pointed at public.api.bsky.app. 512 518 * 513 519 * Per spec §6: "All three methods MUST go through public.api.bsky.app (not 514 520 * bsky.social) — that's the public unauthenticated endpoint with the more 515 521 * generous rate limits." 522 + * 523 + * The optional `fetchImpl` override is used by tests to observe the outbound 524 + * request (e.g. to assert the User-Agent header is set). 516 525 */ 517 - export function createAtprotoClient(onRequest?: AtprotoRequestHook): AtprotoClient { 518 - const agent = new Agent({ service: 'https://public.api.bsky.app' } as never) 526 + export function createAtprotoClient( 527 + onRequest?: AtprotoRequestHook, 528 + fetchImpl?: typeof globalThis.fetch 529 + ): AtprotoClient { 530 + const agent = new Agent({ 531 + service: 'https://public.api.bsky.app', 532 + headers: { 'User-Agent': ATPROTO_USER_AGENT }, 533 + ...(fetchImpl ? { fetch: fetchImpl } : {}), 534 + } as never) 519 535 return new AtprotoClient(agent as unknown as AgentLike, defaultSleep, onRequest) 520 536 }
+20
tests/unit/atproto/client.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 2 import { AtprotoClient, BlueskyRateLimitedError } from '#lib/atproto/index' 3 + import { ATPROTO_USER_AGENT, createAtprotoClient } from '#lib/atproto/client' 3 4 4 5 // --------------------------------------------------------------------------- 5 6 // Mock Agent factory ··· 462 463 assert.strictEqual(rlErr.cause, rateLimitError) 463 464 }).timeout(10000) 464 465 }) 466 + 467 + test.group('createAtprotoClient — User-Agent', () => { 468 + test('sends a favs.blue User-Agent header on AppView requests', async ({ assert }) => { 469 + let capturedUserAgent: string | null = null 470 + const fakeFetch: typeof globalThis.fetch = async (_input, init) => { 471 + const headers = new Headers(init?.headers) 472 + capturedUserAgent = headers.get('user-agent') 473 + return new Response(JSON.stringify({ did: 'did:plc:test' }), { 474 + status: 200, 475 + headers: { 'content-type': 'application/json' }, 476 + }) 477 + } 478 + 479 + const client = createAtprotoClient(undefined, fakeFetch) 480 + await client.resolveHandle('alice.bsky.social') 481 + 482 + assert.equal(capturedUserAgent, ATPROTO_USER_AGENT) 483 + }) 484 + })