Skip to content

fix: surface token parse failures as request errors - #1775

Merged
arthurschreiber merged 2 commits into
tediousjs:masterfrom
james-bg:fix/token-parser-error-routing
Sep 2, 2026
Merged

fix: surface token parse failures as request errors#1775
arthurschreiber merged 2 commits into
tediousjs:masterfrom
james-bg:fix/token-parser-error-routing

Conversation

@james-bg

@james-bg james-bg commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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 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. 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 uncaught Unknown type: 0 stream 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 AbortSignal work, per the maintainer's sequencing.

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 arthurschreiber left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. onParserError can fire after the connection is already in Final (reproduced) and then emits a spurious "No event 'socketError' in state 'Final'" plus a second error after close(). Needs a Final guard or listener removal on state exit.
  2. The once(tokenStreamParser, 'end') / abort races in the login paths leave an unobserved promise that the new 'error' emit can reject.
  3. 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

Comment thread src/connection.ts Outdated
Comment thread src/token/token-stream-parser.ts
Comment thread src/connection.ts
…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.
@james-bg

james-bg commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all three inline items and the SentAttention case are addressed in 7ad2b18.

  • The Final guard now lives in _onSocketError, as you suggested, and the two adjacent readMessage() catches plus both parser-error handlers route through it, so the dispatch/emit sequence is in one place.
  • SentClientRequest detaches the request's cancel/pause/resume listeners before failing it. Your read was exactly right: without that, a late cancel() armed a cancel timer on the closed connection and its expiry hit the same Final hole.
  • The four login/initial-SQL race sites observe their once() promise (same idiom as withAbortRace). No test for this one — the abort tears the socket down before a late parse error can arrive, so I couldn't construct it deterministically; happy to add one if you see a way.
  • SentAttention wraps its once() and routes a rejection the same way, so a malformed attention acknowledgement fails the request instead of crashing.

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 No event 'socketError' in state 'Final' verbatim).

@arthurschreiber arthurschreiber left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this addresses everything. I re-verified on 7ad2b18:

  • The three new tests each fail against the previous commit's connection.ts in the way described, and pass on this head.
  • Lint and tsc are 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 Final guard in _onSocketError could hide a socket error that some path still needs and did not find one: every earlier state has its own socketError handler, and by Final the 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

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.90%. Comparing base (170fabc) to head (7ad2b18).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/connection.ts 75.00% 6 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@arthurschreiber
arthurschreiber merged commit daa8e77 into tediousjs:master Sep 2, 2026
29 of 32 checks passed
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 20.2.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants