-
Notifications
You must be signed in to change notification settings - Fork 35
fix(node): don't fire abort signal on normal request completion #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Previously the abort signal fired on req 'end' event, which occurs when the request body finishes reading - not when the client disconnects. This caused every request to appear aborted. Now listens to res 'close' event and only fires abort if res.writableEnded is false, indicating the connection was terminated before the response completed. Fixes h3js#152
📝 WalkthroughWalkthroughReplaces request Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related issues
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/adapters/_node/request.ts (1)
59-69: Abort wiring now matches desired semantics; consider theres-optional caseThis refactor correctly moves abort signaling off of
req.once("end")and ontoreq.once("error")+res.on("close")with the!res.writableEndedguard, which aligns with the “only on error / client disconnect before response completion” requirement from issue #152 and the PR description. Nice fix.One thing to double‑check:
NodeRequestContext.resis still typed as optional, but_abortControllernow relies entirely onresfor client‑disconnect detection (when there is no request error). IfNodeRequestis ever instantiated with only{ req }(nores),request.signalwill no longer abort on client disconnects in those code paths.You might want to either:
- Make
reseffectively required for all Node server entrypoints that rely onrequest.signal, or- Add a fallback for the
!rescase, e.g. attachreq.once("aborted", abort)orreq.once("close", abort)when noresis provided.That keeps the new semantics while preserving correctness in any request‑only contexts.
Also applies to: 112-135
test/_tests.ts (1)
145-148: Abort count expectation looks fine; avoid noisy console logging in testsThe new
expectedAbortCountlogic (default1, with the Deno special case) matches the current suite behavior and the updated abort semantics.The
console.log(opts.runtime)inside the assertion, though, will run on every test execution and can clutter CI logs. Consider either removing it or gating it behind a debug flag (or leaving the commented‑out detailed log if you need ad‑hoc diagnostics).Also applies to: 157-158
test/node-adapters.test.ts (1)
178-284: Request‑signal behavior tests are good; tighten sync to avoid time‑based flakinessThese three tests nicely pin down the new semantics:
- No abort on successful GET.
- No abort on successful POST, and the body is fully readable.
- Abort fires when the client disconnects mid‑stream.
To make them more robust long‑term:
- Instead of fixed
setTimeout(50/100)sleeps, consider awaiting concrete signals:
- For the disconnect case, store the
fetchpromise andawaitits rejection aftercontroller.abort(); that guarantees the client‑side abort has fully propagated before you assertabortFired.- For the “no abort” cases, an extra microtask (
setImmediate/queueMicrotask) or awaitingserver.close()after the response may be enough, and avoids hardcoded timing.That should reduce the chance of rare timing‑related flakes while keeping the semantics identical.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/adapters/_node/request.ts(3 hunks)test/_tests.ts(2 hunks)test/node-adapters.test.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/adapters/_node/request.ts (2)
src/types.ts (1)
NodeServerResponse(352-354)test/bench-node/_run.mjs (1)
res(47-51)
…handling; clean up tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/adapters/_node/request.ts (1)
112-143: Abort wiring viares.close/req.closenow matches the intended semantics; consider a minor listener tweakThe new
_abortControllersetup correctly:
- Uses a stable
abortControllerreference in the closure.- Aborts on
reqerrors.- For normal adapter usage (with
res), aborts only whenresemitscloseand!res.writableEnded, avoiding the previous “abort on successful completion” behavior.- Falls back to
req.once("close")with a!req.completecheck when noresis available, which is a reasonable best-effort for request-only contexts.This aligns with the PR objective of firing abort only on client disconnects or errors rather than at the end of a successful request.
One small improvement you might consider (non-blocking):
- Use
once("close")for the response as well, to make the one-shot nature of the handler explicit and avoid any risk of multiple invocations ifclosewere ever re-emitted:- if (res) { - // Primary path: detect client disconnect via response close - res.on("close", () => { + if (res) { + // Primary path: detect client disconnect via response close + res.once("close", () => { if (req.errored) { abort(req.errored); } else if (!res.writableEnded) { abort(); } });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/adapters/_node/request.ts(3 hunks)test/_tests.ts(1 hunks)test/node-adapters.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- test/_tests.ts
- test/node-adapters.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/adapters/_node/request.ts (2)
src/types.ts (1)
NodeServerResponse(352-354)test/bench-node/_run.mjs (1)
res(47-51)
🔇 Additional comments (1)
src/adapters/_node/request.ts (1)
55-73: Storing#reson the request to support abort semantics looks correctWiring
ctx.resinto the private#resfield and exposing it viaruntime.nodegives the abort logic access to the response lifecycle without changing the public surface. There are no obvious correctness or lifetime issues with this addition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/adapters/_node/request.ts (1)
112-145: Core fix looks correct.The logic properly distinguishes between:
- Normal request completion (
res.writableEnded === true) → no abort- Client disconnect before response completes (
!res.writableEnded) → abort- Request errors → abort with error
This aligns with the expected behavior described in the issue.
Minor nit: The optional chaining on line 120 is redundant since
abortControlleris guaranteed to be defined at this point (assigned on line 117, from the controller created on line 114).const abort = (err?: any) => { - abortController?.abort?.(err); + abortController.abort(err); };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/adapters/_node/request.ts(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/adapters/_node/request.ts (1)
src/types.ts (1)
NodeServerResponse(352-354)
🔇 Additional comments (2)
src/adapters/_node/request.ts (2)
59-68: LGTM!Clean addition of the response reference. The optional typing correctly reflects that
resmay not be present in all contexts (e.g., request-only scenarios).
134-142: Fallback path is sensible.Using
req.completeto detect incomplete request body reception is the right check for request-only contexts. This ensures abort fires only when the client disconnects mid-request rather than on normal completion.When would this fallback path actually be exercised? Consider adding a brief inline comment explaining which scenarios result in a
NodeRequestContextwithout aresobject, to help future maintainers understand when this code path is taken.
|
Hi! I'm I would like to apply some automated changes to this pull request, but it looks like I don't have the necessary permissions to do so. To get this pull request into a mergeable state, please do one of the following two things:
|
Fixes #152
Problem
The Node adapter was listening to
req.once("end", abort)which fires when the request body is done reading, not when the client disconnects. This caused the abort signal to fire on every single request.Solution
req.once("end", abort)listenerres.on("close")instead!res.writableEnded(connection closed before response completed)This matches how @hono/node-server handles abort signals.
Testing
Added tests in
test/node.test.tsto verify:Summary by CodeRabbit
Bug Fixes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.