Skip to content

fix: allow executing a request again after it was canceled - #1790

Merged
arthurschreiber merged 2 commits into
masterfrom
claude/fix-reset-request-canceled
Sep 23, 2026
Merged

arthurschreiber merged 2 commits into
masterfrom
claude/fix-reset-request-canceled

Conversation

@arthurschreiber

Copy link
Copy Markdown
Collaborator

Request.canceled is only ever set back to false in the constructor. Once a request has been canceled, makeRequest rejects every later execution of it with ECANCEL before sending anything.

For a plain request this means it can't be retried after a cancel. For a prepared statement it's worse: a single canceled execute() makes every later execute() fail, and unprepare() fails too, so the statement leaks on the server.

Changes

  • Request.canceled is reset when an execution completes.
  • The reset happens at the start of the request's internal completion callback, before the user callback and the prepared/error/requestCompleted events run. So the common pattern of running a request again from its own callback works too.
  • A cancel() still affects only the execution in progress, or the next one if it's called before executing. That is unchanged.

Tests

New integration tests:

  • allows executing a request again after it was canceled (test/integration/connection-test.ts)
  • allows executing a prepared statement again after an execution was canceled (test/integration/prepare-execute-statements-test.ts)

Both fail on master with an uncaught Canceled. error when the request runs again.

Checks

  • npm run lint (eslint and tsc)
  • npm run build
  • Unit tests: 533 passing
  • Integration tests: 466 passing, 1 failing. The failure is Initiate Connect Test, which also fails on master in the local test environment.
  • commitlint was not run: it isn't installed in the dev dependencies. The commit message follows the conventional format.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HGJsu1u44CHnt4nkkQ5ias


Generated by Claude Code

`Request.canceled` was only ever reset in the constructor. Once a request
was canceled, every later execution of it failed with `ECANCEL` before
being sent. That broke prepared statements for good after one canceled
execution, including their unpreparing.

The flag is now reset when an execution completes, before the completion
callback and events run, so the request can also be executed again from
within them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGJsu1u44CHnt4nkkQ5ias
@claude

claude Bot commented Sep 23, 2026

Copy link
Copy Markdown

Review

Looked through the diff, the surrounding makeRequest/state-machine code in src/connection.ts, and the token handler's canceled checks to make sure the reset doesn't race with anything.

Correctness: looks right. The root cause is accurately diagnosed — canceled was only ever reset in the constructor, so makeRequest's else if (request.canceled) guard (src/connection.ts:3250) would reject every future execution of a request once cancel() had been called on it once. Resetting the flag inside the shared callback wrapper, before the preparing/userCallback/event-emission branches, is the right place:

  • It covers all three cycles a Request can go through (sp_prepare, execute, sp_unprepare), since they all funnel through the same callback.
  • Resetting it before invoking userCallback/emitting events (rather than after) is what makes the re-execute-from-within-the-callback pattern in the new tests work — nice catch including that ordering explicitly in the code comment.
  • I checked the request.canceled reads in src/token/handler.ts and src/connection.ts (lines ~3336, ~3829) — all of those run before request.callback(...) is invoked in the state machine, so resetting the flag inside the callback doesn't affect any in-flight decision that depends on the old value.

Test coverage: The two new integration tests are good end-to-end reproductions of the reported bug (plain request retry + the prepared-statement leak scenario, including confirming unprepare still works). One suggestion: since these require a live SQL Server connection (and the PR notes one unrelated integration test already fails locally), it'd be worth adding a small, DB-less unit test in test/unit/request-test.ts that calls request.callback(new Error(...)) directly and asserts request.canceled is false afterward. That pins down this specific fix in an environment-independent way and guards against a future regression even where SQL Server isn't available.

Minor/non-blocking edge case: cancel() can still be called while nothing is executing (e.g., between a successful execute() completion and a later unprepare() call), which sets canceled = true with no in-flight request to cancel. The next makeRequest call (e.g. unprepare) would then immediately reject with ECANCEL instead of running, leaking the prepared statement again — same symptom as the bug this PR fixes, just via a different trigger. This isn't introduced by this change (the flag's apply-to-the-next-execution semantics predate it) and is arguably a misuse of cancel() per its doc comment ('Cancels a request while waiting for a server response'), so I wouldn't block on it, but it might be worth a follow-up or a doc-comment clarification.

Overall a well-scoped, well-tested fix with a clear explanation of the ordering rationale in the code comment. No security or performance concerns — this only touches a boolean flag's lifecycle.

@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.50%. Comparing base (2172243) to head (17e8159).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1790      +/-   ##
==========================================
+ Coverage   81.48%   82.50%   +1.01%     
==========================================
  Files          95       95              
  Lines        4862     4863       +1     
  Branches      913      913              
==========================================
+ Hits         3962     4012      +50     
+ Misses        631      575      -56     
- Partials      269      276       +7     

☔ 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 e3f43f7 into master Sep 23, 2026
27 of 28 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 20.3.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

arthurschreiber pushed a commit that referenced this pull request Sep 25, 2026
Master now contains the push parser (#1794) that this branch was built
on, and the fixes for output parameters of prepared statements (#1789)
and for executing a request again after it was canceled (#1790), which
this branch had earlier versions of. The branch's versions of the parser
files are kept (they extend the merged parser), with the `process.nextTick`
fix from #1794. `Request.callback` only resets `canceled` before calling
back, like on master: resetting it afterwards would also clear the
cancellation of an execution that was started from the callback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGJsu1u44CHnt4nkkQ5ias
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.

3 participants