fix(network): guarded redirects survive cleanup rejection (#111156)

* fix(network): preserve redirects when cleanup fails

* test(network): bound redirect response assertion

* test(network): honor stream cancellation contract
This commit is contained in:
Peter Steinberger
2026-07-18 20:48:52 -07:00
committed by GitHub
parent 84ad63ef00
commit 63f8b8454b
3 changed files with 53 additions and 2 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ function redirectResponse(location: string): Response {
ok: false,
status: 302,
headers: makeFetchHeaders({ location }),
body: { cancel: vi.fn() },
body: { cancel: vi.fn(async () => undefined) },
} as unknown as Response;
}
+51
View File
@@ -949,6 +949,57 @@ describe("fetchWithSsrFGuard hardening", () => {
await result.release();
});
it("preserves redirects when response body cancellation rejects", async () => {
const unhandledRejections: unknown[] = [];
const onUnhandledRejection = (reason: unknown) => {
unhandledRejections.push(reason);
};
const cancel = vi.fn(() => {
throw new Error("redirect cancellation failed");
});
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(
new Response(new ReadableStream<Uint8Array>({ cancel }), {
status: 302,
headers: { location: "https://cdn.example.com/asset" },
}),
)
.mockResolvedValueOnce(okResponse("redirected"));
process.on("unhandledRejection", onUnhandledRejection);
let result: Awaited<ReturnType<typeof fetchWithSsrFGuard>> | undefined;
try {
result = await fetchWithSsrFGuard({
url: "https://api.example.com/start",
fetchImpl,
lookupFn: createPublicLookup(),
});
const reader = result.response.body?.getReader();
if (!reader) {
throw new Error("expected redirected response body");
}
try {
const firstChunk = await reader.read();
expect(firstChunk.done).toBe(false);
expect(new TextDecoder().decode(firstChunk.value)).toBe("redirected");
await expect(reader.read()).resolves.toEqual({ done: true, value: undefined });
} finally {
reader.releaseLock();
}
expect(cancel).toHaveBeenCalledOnce();
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(unhandledRejections).toStrictEqual([]);
} finally {
await result?.release();
process.off("unhandledRejection", onUnhandledRejection);
expect(process.listeners("unhandledRejection")).not.toContain(onUnhandledRejection);
}
});
it("strips sensitive headers when redirect crosses origins", async () => {
const lookupFn = createPublicLookup();
const fetchImpl = vi
+1 -1
View File
@@ -698,7 +698,7 @@ async function fetchWithSsrFGuardInternal(
throw new Error("Redirect loop detected");
}
visited.add(nextVisitKey);
void response.body?.cancel();
void response.body?.cancel().catch(() => undefined);
await closeDispatcher(dispatcher);
currentUrl = nextUrl;
continue;