From 1a073916a060e33fc060f0877bcd30eb5122d848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Mon, 19 Oct 2026 14:38:23 +0200 Subject: [PATCH] fix(auto-routing): post owner pool conversions with a redirect mode Workers accept --- .../scripts/convert-owner-pools.ts | 14 +++---- .../src/owner-pool-conversion.test.ts | 7 +++- .../auto-routing/src/owner-pool-conversion.ts | 37 ++++++++++++++++--- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/services/auto-routing/scripts/convert-owner-pools.ts b/services/auto-routing/scripts/convert-owner-pools.ts index c4f42d24496..17532dd566b 100644 --- a/services/auto-routing/scripts/convert-owner-pools.ts +++ b/services/auto-routing/scripts/convert-owner-pools.ts @@ -23,15 +23,15 @@ async function cloudflareGet(token: string, path: string, r const body = z .object({ success: z.boolean(), - errors: z.array(z.object({ message: z.string() })).optional(), - result: z.array(resultSchema).default([]), + errors: z.array(z.object({ message: z.string() })).nullish(), + result: z.array(resultSchema).nullish(), result_info: z .object({ - cursor: z.string().optional(), - page: z.number().int().optional(), - total_pages: z.number().int().optional(), + cursor: z.string().nullish(), + page: z.number().int().nullish(), + total_pages: z.number().int().nullish(), }) - .optional(), + .nullish(), }) .parse(await response.json()); if (!response.ok || !body.success) { @@ -39,7 +39,7 @@ async function cloudflareGet(token: string, path: string, r `Cloudflare ${path.split('?')[0]} responded ${response.status}: ${body.errors?.map(e => e.message).join('; ') ?? ''}` ); } - return body; + return { ...body, result: body.result ?? [] }; } async function findNamespaceId(token: string, accountId: string): Promise { diff --git a/services/auto-routing/src/owner-pool-conversion.test.ts b/services/auto-routing/src/owner-pool-conversion.test.ts index 9d4d2dbafaf..f119d9e8a18 100644 --- a/services/auto-routing/src/owner-pool-conversion.test.ts +++ b/services/auto-routing/src/owner-pool-conversion.test.ts @@ -254,7 +254,7 @@ describe('POST /admin/owner-pool-conversion/scan', () => { 'https://web.example.test/api/internal/auto-routing/owner-pool-conversions' ); expect(new Headers(init?.headers).get('authorization')).toBe('Bearer internal-secret'); - expect(init?.redirect).toBe('error'); + expect(init?.redirect).toBe('manual'); expect(init?.signal).toBeInstanceOf(AbortSignal); expect(webRequests()).toEqual([ { @@ -398,6 +398,11 @@ describe('POST /admin/owner-pool-conversion/scan', () => { it.each([ ['a non-2xx response', async () => new Response('boom', { status: 502 })], + [ + 'a redirect', + async () => + new Response(null, { status: 307, headers: { location: 'https://elsewhere.test/' } }), + ], ['an invalid response', async () => Response.json({ results: [] })], ['a network error', async () => Promise.reject(new Error('connection reset'))], [ diff --git a/services/auto-routing/src/owner-pool-conversion.ts b/services/auto-routing/src/owner-pool-conversion.ts index 344c393a40b..8b1804ad027 100644 --- a/services/auto-routing/src/owner-pool-conversion.ts +++ b/services/auto-routing/src/owner-pool-conversion.ts @@ -101,6 +101,21 @@ async function mapBounded( return results; } +function logPostFailure( + stage: string, + batchSize: number, + detail: Record = {} +): void { + console.warn( + JSON.stringify({ + event: 'auto_routing_owner_pool_conversion_post_failed', + stage, + batchSize, + ...detail, + }) + ); +} + export async function postOwnerPoolConversions( env: Pick, chunk: readonly OwnerPoolConversion[] @@ -113,18 +128,30 @@ export async function postOwnerPoolConversions( headers: { authorization: `Bearer ${secret}`, 'content-type': 'application/json' }, body: JSON.stringify({ conversions: chunk }), signal: AbortSignal.timeout(BATCH_TIMEOUT_MS), - // The internal secret must never follow a redirect. - redirect: 'error', + // Workers reject redirect: 'error'. With 'manual' the internal secret never follows a + // redirect, and a 3xx is not ok, so the batch fails. + redirect: 'manual', }); - if (!response.ok) return null; + if (!response.ok) { + logPostFailure('http_status', chunk.length, { status: response.status }); + return null; + } const parsed = OwnerPoolConversionResponseSchema.safeParse(await response.json()); - if (!parsed.success || parsed.data.results.length !== chunk.length) return null; + if (!parsed.success || parsed.data.results.length !== chunk.length) { + logPostFailure('invalid_response', chunk.length, { status: response.status }); + return null; + } const inOrder = parsed.data.results.every( (result, index) => result.owner.type === chunk[index]?.owner.type && result.owner.id === chunk[index]?.owner.id ); + if (!inOrder) logPostFailure('result_mismatch', chunk.length, { status: response.status }); return inOrder ? parsed.data.results : null; - } catch { + } catch (error) { + logPostFailure('exception', chunk.length, { + errorName: error instanceof Error ? error.name : typeof error, + errorMessage: error instanceof Error ? error.message.slice(0, 300) : null, + }); return null; } }