feat(proxy): support client aborts#1417
Conversation
- client abort signals are propogated upstream - 502 response on abort remains the default to prevent breaking existing behaviour - new option `propogateAbortError` to throw client abort error instead of 502 response
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough
ChangesProxy Abort Handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Key the `propagateAbortError` guard off the caught error being an `AbortError` rather than `event.req.signal.aborted`. This fixes two cases: - a caller-supplied `fetchOptions.signal` now propagates correctly (the client signal is never aborted, so the old check failed) - a real upstream failure coinciding with an aborted signal is now wrapped as 502 instead of being rethrown raw Add a regression test for the custom-signal case. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/proxy.test.ts (1)
265-290: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
it.skipIfper repo test convention instead ofit.runIf.Guidelines mandate
it.skipIf(ctx.target === "node")for runtime-specific skips, notit.runIf(target === "web"). Functionally equivalent today, but diverges from the stated convention and would silently misbehave if a thirddescribeMatrixtarget is ever added (anit.skipIf("node")naturally excludes only node, whereasit.runIf("web")locks the test to web-only, excluding any future target too).♻️ Proposed fix
- it.runIf(t.target === "web")( + it.skipIf(t.target === "node")(As per coding guidelines,
test/**/*.test.ts: "Useit.skipIf(ctx.target === "node")for runtime-specific test skips".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/proxy.test.ts` around lines 265 - 290, The test in proxy.test.ts uses it.runIf(t.target === "web"), which violates the repo’s runtime-skip convention. Update this case to use it.skipIf with the test context target check, matching the existing matrix style and keeping the proxyRequest abort-propagation test named and scoped consistently with the other runtime-specific tests.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@test/proxy.test.ts`:
- Around line 265-290: The test in proxy.test.ts uses it.runIf(t.target ===
"web"), which violates the repo’s runtime-skip convention. Update this case to
use it.skipIf with the test context target check, matching the existing matrix
style and keeping the proxyRequest abort-propagation test named and scoped
consistently with the other runtime-specific tests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3c50752c-fa3f-4ea9-81b1-a4ffde59ebc6
📒 Files selected for processing (2)
src/utils/proxy.tstest/proxy.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/utils/proxy.ts
A client disconnect is not a gateway failure, so by default a proxied request aborted by the client's own signal now resolves to a quiet `499 Client Closed Request` (never delivered, the client is gone) instead of throwing a logged `502` for every dropped connection. `propagateAbortError: true` still lets the `AbortError` propagate to the handler, and abort detection keys off the error (so it also covers a custom `fetchOptions.signal`). The signal is still forwarded to internal sub-request targets as well. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/utils/proxy.ts (1)
112-116: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winCustom
fetchOptions.signalsilently disables client-abort forwarding.Because
...opts.fetchOptionsis spread aftersignal: event.req.signal, any caller-suppliedfetchOptions.signalcompletely replaces the forwarded client signal. That contradicts the new doc comment's claim that the incoming signal is "always forwarded" (Lines 25-27) — in practice, once a custom signal is set, client disconnects no longer abort the upstream request at all, defeating the feature this PR introduces.Consider merging the two signals with
AbortSignal.any()so the client abort is always honored regardless of a custom signal:♻️ Proposed fix using AbortSignal.any
const fetchOptions: RequestInit = { - signal: event.req.signal, headers: opts.headers as HeadersInit, ...opts.fetchOptions, + signal: opts.fetchOptions?.signal + ? AbortSignal.any([event.req.signal, opts.fetchOptions.signal]) + : event.req.signal, };Also worth relaying: this is also related to the still-open feedback from
iainsproaton the previous commit asking whether forwarding should be gated behind an option — the forwarding remains unconditional in this version.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/proxy.ts` around lines 112 - 116, The proxy request setup in `src/utils/proxy.ts` currently lets `opts.fetchOptions.signal` overwrite `event.req.signal`, so client-abort forwarding is lost whenever a custom signal is provided. Update the `fetchOptions` assembly in the proxy helper to preserve the incoming request signal and combine it with any caller-supplied signal, ideally in the same place where `fetchOptions` is built. Use the `event.req.signal` and `opts.fetchOptions.signal` values together so upstream requests still abort on client disconnect even when custom fetch options are passed, and make sure the behavior matches the doc comment’s “always forwarded” guarantee or add a gate if that’s the intended contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/utils/proxy.ts`:
- Around line 112-116: The proxy request setup in `src/utils/proxy.ts` currently
lets `opts.fetchOptions.signal` overwrite `event.req.signal`, so client-abort
forwarding is lost whenever a custom signal is provided. Update the
`fetchOptions` assembly in the proxy helper to preserve the incoming request
signal and combine it with any caller-supplied signal, ideally in the same place
where `fetchOptions` is built. Use the `event.req.signal` and
`opts.fetchOptions.signal` values together so upstream requests still abort on
client disconnect even when custom fetch options are passed, and make sure the
behavior matches the doc comment’s “always forwarded” guarantee or add a gate if
that’s the intended contract.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 30853529-3cd3-4c20-9ed0-f76f6aedf9e4
📒 Files selected for processing (2)
src/utils/proxy.tstest/proxy.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- test/proxy.test.ts
…mid-stream aborts - Merge event.req.signal with a caller-supplied fetchOptions.signal instead of letting the object spread drop it, so client disconnects always abort upstream. - Wrap the proxied response body so a client-caused abort during streaming ends the stream quietly (499 default) instead of surfacing an unhandled stream error. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/proxy.ts (1)
188-231: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffResponse body is wrapped on the default hot path for every proxied response.
Since
propagateAbortErrordefaults to false,quietlyAbortableStreamwraps the upstream body for all successful responses, not just aborted ones. Each chunk is now pumped through a JSpullloop (reader.read()→enqueue) instead of handing the native stream straight toHTTPResponse, adding per-chunk overhead and losing byte-stream (type: "bytes"/BYOB) semantics. For a proxy this is a throughput-sensitive path — worth measuring, or scoping the wrap to only kick in on abort.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/proxy.ts` around lines 188 - 231, The default proxy response path in `src/utils/proxy.ts` always wraps successful upstream bodies with `quietlyAbortableStream`, which adds per-chunk JS pumping overhead and drops native byte-stream/BYOB behavior even when no abort occurs. Update the `HTTPResponse` body selection logic so `quietlyAbortableStream` is only used when abort handling is actually needed, or otherwise preserve the original `response.body` on the hot path while keeping the abort-suppression behavior in `quietlyAbortableStream` for client disconnects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/utils/proxy.ts`:
- Around line 188-231: The default proxy response path in `src/utils/proxy.ts`
always wraps successful upstream bodies with `quietlyAbortableStream`, which
adds per-chunk JS pumping overhead and drops native byte-stream/BYOB behavior
even when no abort occurs. Update the `HTTPResponse` body selection logic so
`quietlyAbortableStream` is only used when abort handling is actually needed, or
otherwise preserve the original `response.body` on the hot path while keeping
the abort-suppression behavior in `quietlyAbortableStream` for client
disconnects.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6f72496b-8802-406d-a2cf-e6c6d5202425
📒 Files selected for processing (2)
src/utils/proxy.tstest/proxy.test.ts
Pass the upstream body straight to HTTPResponse instead of wrapping every proxied response in a per-chunk read/enqueue loop. A mid-stream client abort errors the upstream body, which is delivered to the stream consumer (the server runtime, already tearing down the closed connection) — so it needs no special handling and never becomes a 502. Removes per-chunk overhead from all proxied traffic. Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
Thanks! |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/proxy.test.ts (1)
316-351: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winMissing Node-target coverage for the "no special handling" streaming-abort assumption.
This test — and all the other new abort tests in this block — only run
it.runIf(t.target === "web"). Given the streaming change insrc/utils/proxy.ts(removingquietlyAbortableStream) explicitly relies on "the runtime" absorbing the errored body stream, and Node's stream error handling differs materially from the browser/web-stream model, this is exactly the scenario that needs a Node-target equivalent to catch a potential crash regression. As per coding guidelines,test/**/*.test.tsshould "Test across runtimes usingdescribeMatrixto ensure compatibility."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/proxy.test.ts` around lines 316 - 351, The new abort-stream coverage in proxy.test.ts is web-only, but the proxyRequest/quietlyAbortableStream change in src/utils/proxy.ts needs equivalent Node-runtime validation too. Add a Node-target test case for the same mid-stream AbortError scenario alongside the existing it.runIf(t.target === "web") block, using describeMatrix or an equivalent target split so the behavior is asserted in both runtimes. Keep the assertions aligned with the existing proxyRequest and t.fetch flow: upstream response should still come through as 200, and the stream error should be handled by the consumer rather than becoming a gateway error or crash.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@test/proxy.test.ts`:
- Around line 316-351: The new abort-stream coverage in proxy.test.ts is
web-only, but the proxyRequest/quietlyAbortableStream change in
src/utils/proxy.ts needs equivalent Node-runtime validation too. Add a
Node-target test case for the same mid-stream AbortError scenario alongside the
existing it.runIf(t.target === "web") block, using describeMatrix or an
equivalent target split so the behavior is asserted in both runtimes. Keep the
assertions aligned with the existing proxyRequest and t.fetch flow: upstream
response should still come through as 200, and the stream error should be
handled by the consumer rather than becoming a gateway error or crash.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f82808f9-74c6-4634-ae1a-239c28f6feaa
📒 Files selected for processing (2)
src/utils/proxy.tstest/proxy.test.ts
propogateAbortErrorto throw client abort error instead of 502 responseSummary by CodeRabbit
propagateAbortErroroption to control whether client disconnects surface as anAbortErroror are handled quietly.fetchsignal.propagateAbortErrorsurfaces theAbortError. Mid-stream disconnects no longer trigger gateway errors.propagateAbortError: true, and mid-stream abort outcomes.