fix: release the discarded discovery response on every throw (#2319 round 22)
The timeout and cancellation exits added over the last few rounds drop the original failed discovery response, which nobody will ever receive — and an unread body holds its connection open on Node/undici. The successful-substitution path already released it for that reason; the five exceptional exits now do too (pre-abort, timeout, fetch-abort, post-release abort, body-read abort). Matters most for a standalone user of the exported wrapper, where no timeout wrapper underneath has already drained it. Also drops the last descriptions of the superseded rebuild: the test file's header, an inline comment asserting the response is "not the same object" when the file later asserts identity, and the PR description's account of copied metadata and deleted encoding headers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
bb01c1e73b
commit
45d87911eb
2 changed files with 71 additions and 11 deletions
|
|
@ -543,6 +543,41 @@ describe("probe cancellation (#2319)", () => {
|
|||
expect(await settled).toBe(reason);
|
||||
});
|
||||
|
||||
it("releases the discarded discovery response on an exceptional exit", async () => {
|
||||
// Nobody receives `response` when the loop throws, and an unread body holds
|
||||
// its connection open on Node/undici — the same reason the successful
|
||||
// substitution path releases it.
|
||||
const cancels = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
|
||||
const discovery = new Response(
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(ctrl) {
|
||||
ctrl.enqueue(new TextEncoder().encode("not found"));
|
||||
},
|
||||
}),
|
||||
{ status: 404 },
|
||||
);
|
||||
// Spy on the body's cancel, which is what `releaseBody` calls.
|
||||
const body = discovery.body;
|
||||
if (body) vi.spyOn(body, "cancel").mockImplementation(cancels);
|
||||
|
||||
const inner = vi.fn<typeof fetch>((input) =>
|
||||
String(input) === RFC8414
|
||||
? Promise.resolve(discovery)
|
||||
: new Promise<Response>(() => {}),
|
||||
);
|
||||
const wrapped = withRfc8414OidcCompat(inner);
|
||||
|
||||
const settled = wrapped(RFC8414, {
|
||||
signal: AbortSignal.abort(new Error("gone")),
|
||||
}).then(
|
||||
() => "resolved",
|
||||
() => "threw",
|
||||
);
|
||||
|
||||
expect(await settled).toBe("threw");
|
||||
expect(cancels).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still falls back to the original response on an ordinary probe failure", async () => {
|
||||
const inner = vi.fn<typeof fetch>((input) => {
|
||||
if (String(input) === RFC8414) {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
* - **The bound itself**, including the *body*: `fetch` resolves on headers, so
|
||||
* a server that sends them and then stalls has to be caught by the buffering
|
||||
* race, not by the fetch promise.
|
||||
* - **The rebuilt response.** Buffering means the caller gets a different
|
||||
* `Response` object, so `url` / `redirected` / `type` are asserted to survive
|
||||
* and `content-encoding` / `content-length` to be dropped — the buffer holds
|
||||
* the decoded body, which makes both of those headers lies.
|
||||
* - **The response's identity.** The bound drains a *clone* and hands back the
|
||||
* original, so the cases assert identity, that the body is still readable
|
||||
* after the drain, and that a caller's own `clone()` keeps its metadata —
|
||||
* the property a rebuilt response could not have, since `url` / `redirected`
|
||||
* / `type` are internal slots.
|
||||
* - **Cancellation semantics.** The caller's signal is forwarded *and* raced,
|
||||
* so the tests distinguish the two: one uses a fetch that honours the signal,
|
||||
* another uses one that ignores it entirely.
|
||||
|
|
@ -44,8 +45,9 @@ afterEach(() => {
|
|||
|
||||
describe("withOAuthRequestTimeout", () => {
|
||||
it("passes a prompt response through", async () => {
|
||||
// Not the same object: the body is buffered under the deadline (see the
|
||||
// stalled-body case below) and the response rebuilt around it.
|
||||
// The same object: the body is drained through a clone under the deadline
|
||||
// (see the stalled-body case below), so nothing about the response the
|
||||
// caller receives changes.
|
||||
const inner = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValue(new Response('{"ok":true}', { status: 200 }));
|
||||
|
|
|
|||
Loading…
Reference in a new issue