diff --git a/src/proxy/handler.test.ts b/src/proxy/handler.test.ts index 54d9247480..c1980d9087 100644 --- a/src/proxy/handler.test.ts +++ b/src/proxy/handler.test.ts @@ -442,6 +442,7 @@ describe("Proxy Handler", () => { }); it("fails closed when routing metadata has an operational failure", async () => { + const { entries, logger } = createRecordingLogger(); let fullProjectLookups = 0; const { server, port } = createMockServer((req: Request) => { const { pathname } = new URL(req.url); @@ -467,7 +468,7 @@ describe("Proxy Handler", () => { }); try { - const handler = createHandler(port); + const handler = createHandler(port, "", logger); const ctx = await handler.processRequest( new Request("http://example.com/page", { headers: { host: "example.com" }, @@ -477,6 +478,14 @@ describe("Proxy Handler", () => { assertEquals(ctx.error?.status, 502); assertEquals(ctx.error?.message, "Proxy routing metadata request was rejected"); assertEquals(fullProjectLookups, 0); + + const failedClosed = entries.find((entry) => + entry.message === "Proxy metadata lookup failed closed" + ); + assertEquals(failedClosed?.level, "error"); + assertEquals(failedClosed?.extra?.upstreamStatus, 503); + assertEquals(failedClosed?.extra?.upstreamBodySnippet, undefined); + assertEquals(JSON.stringify(failedClosed).includes("Unavailable"), false); await handler.close(); } finally { await server.shutdown(); diff --git a/src/proxy/handler.ts b/src/proxy/handler.ts index b4d19a3202..4439945982 100644 --- a/src/proxy/handler.ts +++ b/src/proxy/handler.ts @@ -909,6 +909,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) { scope, lookupType: error.lookupType, status: error.publicStatus, + upstreamStatus: error.upstreamStatus, }); return { error: { @@ -975,6 +976,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) { scope, lookupType: retryError.lookupType, status: retryError.publicStatus, + upstreamStatus: retryError.upstreamStatus, }); return { error: { diff --git a/src/proxy/project-metadata-client.test.ts b/src/proxy/project-metadata-client.test.ts index b3331778ff..fc9ad2ec97 100644 --- a/src/proxy/project-metadata-client.test.ts +++ b/src/proxy/project-metadata-client.test.ts @@ -74,6 +74,57 @@ describe("proxy project metadata client", () => { ProxyLookupFailure, ); assertEquals((failure as ProxyLookupFailure).publicStatus, 502); + assertEquals((failure as ProxyLookupFailure).upstreamStatus, 500); + }); + + it("records only non-sensitive upstream diagnostics", async () => { + const rejected = createProjectMetadataClient({ + apiBaseUrl: "https://api.example.com", + fetchImpl: makeFetch(() => + new Response(`{"error":"database unavailable"}${"x".repeat(512)}`, { status: 500 }) + ), + }); + const failure = await assertRejects( + () => rejected.lookupAccess("storefront", "token", false), + ProxyLookupFailure, + "Proxy access metadata request was rejected", + ) as ProxyLookupFailure; + assertEquals(failure.publicStatus, 502); + assertEquals(failure.upstreamStatus, 500); + assertEquals("upstreamBodySnippet" in failure, false); + assertEquals(failure.message.includes("500"), false); + + const wrongContentType = createProjectMetadataClient({ + apiBaseUrl: "https://api.example.com", + fetchImpl: makeFetch(() => + new Response("maintenance", { + headers: { "Content-Type": "text/html" }, + }) + ), + }); + const contentTypeFailure = await assertRejects( + () => wrongContentType.lookupRouting("storefront", "token"), + ProxyLookupFailure, + "content type", + ) as ProxyLookupFailure; + assertEquals(contentTypeFailure.upstreamStatus, 200); + assertEquals("upstreamBodySnippet" in contentTypeFailure, false); + + const invalidJson = createProjectMetadataClient({ + apiBaseUrl: "https://api.example.com", + fetchImpl: makeFetch(() => + new Response("not json", { + headers: { "Content-Type": "application/json" }, + }) + ), + }); + const invalidFailure = await assertRejects( + () => invalidJson.lookupRouting("storefront", "token"), + ProxyLookupFailure, + "invalid response", + ) as ProxyLookupFailure; + assertEquals(invalidFailure.upstreamStatus, 200); + assertEquals("upstreamBodySnippet" in invalidFailure, false); }); it("requires bounded JSON with the expected response schema", async () => { diff --git a/src/proxy/project-metadata-client.ts b/src/proxy/project-metadata-client.ts index 9398b3c35d..e3773ed3c8 100644 --- a/src/proxy/project-metadata-client.ts +++ b/src/proxy/project-metadata-client.ts @@ -93,15 +93,23 @@ export class ProxyLookupAuthError extends Error { } } +export interface ProxyLookupFailureUpstreamContext { + /** HTTP status returned by the metadata API for the failed lookup. */ + upstreamStatus?: number; +} + export class ProxyLookupFailure extends Error { + readonly upstreamStatus?: number; + constructor( readonly lookupType: ProxyLookupType, readonly publicStatus: 502 | 503 | 504, message: string, - options?: ErrorOptions, + options?: ErrorOptions & ProxyLookupFailureUpstreamContext, ) { super(message, options); this.name = "ProxyLookupFailure"; + this.upstreamStatus = options?.upstreamStatus; } } @@ -518,6 +526,7 @@ export function createProjectMetadataClient( lookupType, publicStatus, `Proxy ${lookupType} metadata request was rejected`, + { upstreamStatus: response.status }, ); } @@ -529,6 +538,7 @@ export function createProjectMetadataClient( lookupType, 502, `Proxy ${lookupType} metadata returned an invalid content type`, + { upstreamStatus: response.status }, ); } @@ -547,7 +557,10 @@ export function createProjectMetadataClient( lookupType, 502, `Proxy ${lookupType} metadata returned an invalid response`, - { cause: error }, + { + cause: error, + upstreamStatus: response.status, + }, ); } } catch (error) {