fix: do not await the discarded stream's cancel in /api/fetch (#2319 round 26)

Third and last site of the same class. `ReadableStream.cancel()` adopts
the underlying source's cancel promise, which is permitted never to
settle, so awaiting it kept the handler pending forever — and on an
unbounded request there is no timer to release it either. The cancel is
started and the handler returns, so the JSON response and the `finally`
complete regardless.

The `void` carries the justification for which of AGENTS.md's permitted
cases it is: the callee owns its failures through its own `catch`, and
the caller cannot await.

Test: a `/stream-hostile` upstream that sends event-stream headers and
one frame and then never closes, asserting the route still answers 200
with no body.

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:
cliffhall 2026-09-11 08:47:01 -04:00
commit cf856c740c

View file

@ -1323,7 +1323,14 @@ export function createRemoteApp(
// its own in this configuration — so this is belt and braces rather
// than a demonstrated leak, and it is kept because relying on that is
// an implementation detail of the fetch beneath us, not a contract.
await res.body?.cancel().catch(() => {});
//
// ⚠️ Started, not awaited. `ReadableStream.cancel()` adopts the
// underlying source's cancel promise, which is permitted never to
// settle — awaiting it would keep this handler pending forever, and on
// an unbounded request there is no timer to release it either
// (Copilot). The `void` is the documented case where the callee owns
// its failures, via the `catch` below, and the caller cannot await.
void res.body?.cancel().catch(() => {});
}
return c.json({