Fix double callback invoke on unhandled exception - #528
Conversation
|
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:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughThis change prevents ChangesCallback exception handling
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Callback exceptions now propagate after a single callback invocation, preventing duplicate completion handling. The covered behavior is ready to merge. 🚥 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 |
The callback was invoked inside the `try`, so an exception thrown by the callback itself landed in the `catch` and invoked it a second time with its own error as `err`. Narrow the `try` to cover only `syncVersion`. This keeps the callback synchronous. Deferring it with `process.nextTick` also stops the double invocation, but changes when the callback runs: `computeSignature(xml, cb)` would return before `cb` fires, so an existing caller reading `getSignedXml()` immediately after gets `""` rather than the signed document -- a silent breaking change for a public, semver-bound API. De-Zalgo-ing these callbacks is worth doing, but as a deliberate major. The `return` in the `catch` is compiler-enforced: without it `result` is not definitely assigned and `tsc --strict` rejects the code, so the error path cannot regress into a double invocation. Replace the helper-level tests with one driving the public `computeSignature(xml, callback)` path from the issue. It is synchronous, so it no longer removes and restores the process `uncaughtException` listeners, which leaked mocha's handler when the test failed. Resolves node-saml#527 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@adamjmcgrath , I've made some changes. What do you think? |
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (66.66%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## master #528 +/- ##
==========================================
+ Coverage 75.95% 76.41% +0.46%
==========================================
Files 9 9
Lines 1048 1060 +12
Branches 273 275 +2
==========================================
+ Hits 796 810 +14
+ Misses 144 143 -1
+ Partials 108 107 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Reported and diagnosed by @adamjmcgrath in #527: when a caller's callback throws,
computeSignature(xml, cb)invokes it a second time, passing the callback's own error back aserr.Cause
createOptionalCallbackFunctioninvoked the callback inside thetry, so an exception thrown by the callback landed in thecatchand was handed straight back to it:Present since the helper was introduced in #343, so every release from v4.0.0 onward.
Fix
Narrow the
tryto cover onlysyncVersion. Once the callback is outside it, its exceptions cannot re-enter thecatch:The
returnis enforced by the compiler rather than by discipline — without itresultis not definitely assigned andtsc --strictrejects the file, so the error path cannot regress into a double call.Why not
process.nextTickThe original version of this PR deferred the success callback with
process.nextTick. That also stops the double invocation, but it changes when the callback runs, andcomputeSignature(xml, cb)was effectively synchronous. Measured on the same input, the one line differing:getSignedXml()immediately afterprocess.nextTickAn existing caller reading
getSignedXml()aftercomputeSignature(xml, cb)would get an empty string rather than an exception — a silent breaking change to a semver-bound public API, in a library where the failure surfaces downstream as an unsigned document. Narrowing thetryfixes the reported bug with byte-identical timing instead.De-Zalgoing these callbacks is still worth doing, but as a deliberate major rather than inside a bug fix. Tracked for 7.0 in #546, alongside #545.
Tests
One regression test driving the public
computeSignature(xml, callback)path from the issue. It was watched failing first against the unfixed helper, for the reported reason:It is fully synchronous, so it no longer removes and restores the process
uncaughtExceptionlisteners — the earlier version leaked mocha's handler for the rest of the run whenever it failed.npm run build,npm test(219 passing) andnpm run lintall clean.fixes #527
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests