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
11 changes: 10 additions & 1 deletion src/proxy/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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" },
Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/proxy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
scope,
lookupType: error.lookupType,
status: error.publicStatus,
upstreamStatus: error.upstreamStatus,
});
return {
error: {
Expand Down Expand Up @@ -975,6 +976,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
scope,
lookupType: retryError.lookupType,
status: retryError.publicStatus,
upstreamStatus: retryError.upstreamStatus,
});
return {
error: {
Expand Down
51 changes: 51 additions & 0 deletions src/proxy/project-metadata-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html>maintenance</html>", {
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 () => {
Expand Down
17 changes: 15 additions & 2 deletions src/proxy/project-metadata-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -518,6 +526,7 @@ export function createProjectMetadataClient(
lookupType,
publicStatus,
`Proxy ${lookupType} metadata request was rejected`,
{ upstreamStatus: response.status },
);
}

Expand All @@ -529,6 +538,7 @@ export function createProjectMetadataClient(
lookupType,
502,
`Proxy ${lookupType} metadata returned an invalid content type`,
{ upstreamStatus: response.status },
);
}

Expand All @@ -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) {
Expand Down
Loading