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✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #528 +/- ##
==========================================
+ Coverage 75.95% 77.35% +1.39%
==========================================
Files 9 9
Lines 1048 1073 +25
Branches 273 275 +2
==========================================
+ Hits 796 830 +34
+ Misses 144 137 -7
+ Partials 108 106 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Ensures the `computeSignature` callback is invoked exactly once with an error, and `getSignedXml()` returns an empty string, when the cryptographic signing operation fails. This test complements existing error handling tests by specifically covering failures originating from the signing process itself, such as using an invalid key.
markstos
left a comment
There was a problem hiding this comment.
I agree this is a bug and agree with the fix. Test coverage also looks good.
The added comments are unnecessary, though.
In most cases, comments that describe bugs that the code no longer has are not useful. Git history is useful to reviewing prior states of the code and history of code changes.
Resolve index.ts conflict by keeping this branch's explicit re-export lists (replacing the `export *` wildcards) while adopting master's util.deprecate() wrapping for the helpers withdrawn in 7.0 (node-saml#551). findAncestorNs and findAncestorNsForNode stay plain exports since neither is on the deprecation list. Also update the new Callback invocation test (merged in from master's node-saml#528 fix) to declare the enveloped-signature transform, since it signs a self-enclosing reference and otherwise trips the encloses-the- signature-without-a-transform guard added in ce8d32a.
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