fix: surface token parse failures as request errors - #1775
Conversation
The token parser's internal stream had no 'error' listener, so a parse failure inside a response surfaced as an unhandled 'error' event and crashed the process instead of failing the request. Re-emit stream errors from the token parser, and route them in SentClientRequest through the existing socket error path: the active request fails with the parse error and the connection is closed - a parse failure leaves the connection at an undefined position in the TDS stream, so it cannot be recovered at the request level. This complements tediousjs#1759, which fixed the same unhandled-error hole for the login path.
arthurschreiber
left a comment
There was a problem hiding this comment.
Thanks for splitting this out. The core change is correct and the test covers it: with the src changes reverted the new test dies with an uncaught Unknown type: 0, and with them it passes. Full unit suite is green on my side too.
Three things I'd like addressed before merging, in the inline comments:
onParserErrorcan fire after the connection is already inFinal(reproduced) and then emits a spurious "No event 'socketError' in state 'Final'" plus a second error afterclose(). Needs aFinalguard or listener removal on state exit.- The
once(tokenStreamParser, 'end')/ abort races in the login paths leave an unobserved promise that the new'error'emit can reject. - Request listeners are left attached on the failed request.
One more that is outside the diff but is the same bug one state over: SentAttention (around connection.ts:3880) does await once(tokenStreamParser, 'end') on its own parser, and the surrounding IIFE's .catch rethrows via process.nextTick. With Parser now re-emitting 'error', a malformed token in the attention-ack response rejects that once() and crashes the process, and the request callback is never invoked. Since this PR's stated goal is "surface token parse failures as request errors", I think that path should get the same socketError routing, either here or in a follow-up you're happy to own.
Not blocking: the copied buildLoginAckToken / drainMessage helpers in the test match what the sibling tests do, so no objection there.
Generated by Claude Code
…steners, cover SentAttention Review follow-ups on tediousjs#1775: - `_onSocketError` now ignores errors once the connection is in `Final` (mirrors `socketEnd()`), so a parse failure that lands after `close()` no longer emits "No event 'socketError' in state 'Final'" plus a second error after the connection was closed. The two adjacent `readMessage()` catches and the parser error handlers route through it, so the socket-error sequence lives in one place. - `SentClientRequest` detaches the request's `cancel`/`pause`/`resume` listeners before failing it, so a late `cancel()` cannot send an attention packet to the destroyed socket or arm a cancel timer on a closed connection. - `SentAttention` catches a rejected `once(tokenStreamParser, 'end')` and routes it the same way: a malformed attention acknowledgement fails the request instead of crashing the process. - The login and initial-SQL `Promise.race` sites observe their `once()` promise, so an abort followed by a late parse error cannot become an unhandled rejection. Tests cover the close-from-handler, late-cancel and malformed-ack cases; each fails on the previous commit in the way described in the review.
|
Thanks for the thorough review — all three inline items and the
New tests cover the close-from-handler, late-cancel and malformed-ack cases; each fails on the previous commit in the way you described (the first reproduces your |
arthurschreiber
left a comment
There was a problem hiding this comment.
Thanks, this addresses everything. I re-verified on 7ad2b18:
- The three new tests each fail against the previous commit's
connection.tsin the way described, and pass on this head. - Lint and
tscare clean, the unit suite is green here, and it stays green on a trial merge with current master (the branch is behind #1773, but merges cleanly). - I looked for a case where the
Finalguard in_onSocketErrorcould hide a socket error that some path still needs and did not find one: every earlier state has its ownsocketErrorhandler, and byFinalthe connection is already torn down.
On the untested abort-then-parse-error race: I agree it isn't deterministically constructible, since the abort destroys the socket before a late message can arrive. The no-op catch is the right defensive measure without a test.
LGTM.
Generated by Claude Code
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1775 +/- ##
==========================================
- Coverage 81.02% 80.90% -0.12%
==========================================
Files 92 92
Lines 4948 4950 +2
Branches 938 933 -5
==========================================
- Hits 4009 4005 -4
- Misses 640 657 +17
+ Partials 299 288 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
🎉 This PR is included in version 20.2.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Split out of #1765 per the sequencing discussed there — this is the part accepted as a pure bugfix.
The token parser's internal stream (
Readable.from(StreamParser.parseTokens(...))) has no'error'listener, so a parse failure inside a response surfaces as an unhandled'error'event and crashes the process instead of failing the request.This PR re-emits stream errors from
Parser, and routes them inSentClientRequestthrough the existing socket-error path: the active request fails with the parse error and the connection is closed — a parse failure leaves the connection at an undefined position in the TDS stream, so it cannot be recovered at the request level. The listener is removed at end-of-message; during a canceled response's drain it stays attached, so a parse failure there is still surfaced.Complements #1759, which fixed the same unhandled-
'error'hole for the login path.Test:
connection-parse-error-test.ts— a scripted server answers a SQL batch with an invalid token type (0x00). On current master the test dies with an uncaughtUnknown type: 0stream error; with this fix the request errors and the process stays alive. Full unit suite and lint pass.The inactivity-timer half of #1765 will follow separately, targeted at v21 on top of the
AbortSignalwork, per the maintainer's sequencing.