From 6a4232dbac951b00ee5dfcc48d0c23a0dba070e5 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Wed, 26 Aug 2026 11:48:57 -0400 Subject: [PATCH] Handle transient errors during PDS detection --- .../queries/__tests__/pds-detection.test.ts | 43 +++++++++++++++++++ src/state/queries/pds-detection.ts | 21 ++++----- 2 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 src/state/queries/__tests__/pds-detection.test.ts diff --git a/src/state/queries/__tests__/pds-detection.test.ts b/src/state/queries/__tests__/pds-detection.test.ts new file mode 100644 index 0000000000..c664fedcff --- /dev/null +++ b/src/state/queries/__tests__/pds-detection.test.ts @@ -0,0 +1,43 @@ +import {getMain, XrpcResponseError} from '@atproto/lex' +import {beforeEach, describe, expect, it, jest} from '@jest/globals' + +import {com} from '#/lexicons' + +const mockCall = jest.fn<(...args: unknown[]) => Promise>() + +jest.mock('#/state/session/clients', () => ({ + getPublicAppviewClient: () => ({call: mockCall}), +})) + +import {resolvePdsForIdentifier} from '../pds-detection' + +function responseError(status: number) { + return new XrpcResponseError( + getMain(com.atproto.identity.resolveHandle), + new Response(null, {status}), + undefined, + ) +} + +describe('resolvePdsForIdentifier', () => { + beforeEach(() => { + mockCall.mockReset() + }) + + it.each([429, 500, 502, 503])( + 'surfaces a transient resolveHandle %s response as a network failure', + async status => { + mockCall.mockRejectedValueOnce(responseError(status)) + + await expect(resolvePdsForIdentifier('samuel.fm')).rejects.toThrow( + `Network request failed: resolveHandle returned ${status}`, + ) + }, + ) + + it('treats a permanent resolveHandle 4xx response as unresolved', async () => { + mockCall.mockRejectedValueOnce(responseError(400)) + + await expect(resolvePdsForIdentifier('missing.test')).resolves.toBeNull() + }) +}) diff --git a/src/state/queries/pds-detection.ts b/src/state/queries/pds-detection.ts index 424cc404f0..3d72d495c7 100644 --- a/src/state/queries/pds-detection.ts +++ b/src/state/queries/pds-detection.ts @@ -3,12 +3,13 @@ import {type DidDocument, getPdsEndpoint} from '@atproto/common-web' import {type HandleString} from '@atproto/syntax' import {useQuery, useQueryClient} from '@tanstack/react-query' -import {DEFAULT_SERVICE, PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {DEFAULT_SERVICE} from '#/lib/constants' import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' -import {createServiceClient} from '#/lib/lexClient' import {isNetworkError} from '#/lib/strings/errors' +import {getErrorStatus} from '#/lib/xrpc-error' import {logger} from '#/logger' import {STALE} from '#/state/queries' +import {getPublicAppviewClient} from '#/state/session/clients' import {com} from '#/lexicons' const RQKEY_ROOT = 'pds-detection' @@ -149,14 +150,7 @@ export async function resolvePdsForIdentifier( identifier: string, ): Promise<{did: string; pdsUrl: string | null} | null> { const norm = normalizeIdentifier(identifier) - /* - * This is a pre-auth request. Use an isolated service client rather than the - * shared public appview client, whose global moderation configuration is - * intended for appview content reads and can change independently of this - * login flow. This also matches the pre-auth handle resolution used during - * signup. - */ - const client = createServiceClient(PUBLIC_BSKY_SERVICE) + const client = getPublicAppviewClient() try { let did: string if (norm.startsWith('did:')) { @@ -194,6 +188,13 @@ export async function resolvePdsForIdentifier( isNetworkError: isNetworkError(err), }) if (isNetworkError(err)) throw err + const status = getErrorStatus(err) + if (status !== undefined && isTransientHttpStatus(status)) { + throw new Error( + `Network request failed: resolveHandle returned ${status}`, + {cause: err}, + ) + } return null } }