Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions services/auto-routing/scripts/convert-owner-pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ async function cloudflareGet<T extends z.ZodType>(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) {
throw new Error(
`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<string> {
Expand Down
7 changes: 6 additions & 1 deletion services/auto-routing/src/owner-pool-conversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down Expand Up @@ -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'))],
[
Expand Down
37 changes: 32 additions & 5 deletions services/auto-routing/src/owner-pool-conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ async function mapBounded<T, R>(
return results;
}

function logPostFailure(
stage: string,
batchSize: number,
detail: Record<string, unknown> = {}
): void {
console.warn(
JSON.stringify({
event: 'auto_routing_owner_pool_conversion_post_failed',
stage,
batchSize,
...detail,
})
);
}

export async function postOwnerPoolConversions(
env: Pick<Env, 'INTERNAL_API_SECRET_PROD' | 'KILO_WEB_API_BASE_URL'>,
chunk: readonly OwnerPoolConversion[]
Expand All @@ -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;
}
}
Expand Down