fix: extend abort signal coverage through response body consumption - #12
Merged
Merged
Conversation
The fetchWithTimeout() helper in effect-operator-kit clears the timer as soon as fetch() resolves with response headers, leaving response.text() uncovered. A stalled body stream therefore bypasses the configured timeout. Both AdGuardClient and AdGuardSyncClient now use a fetchWithPlainHeaders wrapper that: 1. Awaits fetch() headers as before. 2. Races response.text() against the same AbortSignal that fetchWithTimeout created. When the timer fires and aborts the controller, the body-read Promise rejects with AbortError before text() completes. 3. Returns a synthetic Response with the pre-read body text, so the rest of the effect-operator-kit pipeline is unchanged. Adds regression tests for the stalled-body scenario in client.test.ts and sync-client.test.ts. All 178 tests pass, typecheck clean, build ok. Co-authored-by: solomonneas <41877493+solomonneas@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Fix regression causing response body to stall beyond request timeout
fix: extend abort signal coverage through response body consumption
Aug 15, 2026
Rejecting the body promise on abort left response.text() reading in the background, so the stalled body the timeout fired on kept consuming the connection. Cancelling response.body is what actually releases it. Also carries statusText through the reconstructed Response; callers surface it in error messages and it was being dropped. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a timeout regression where request timeouts stopped applying once fetch() returned response headers, allowing stalled response bodies to hang indefinitely. It does so by ensuring the fetch wrapper does not resolve until the response body has been consumed (or the shared abort signal fires), and adds regression coverage for the “headers arrive, body stalls” scenario.
Changes:
- Update both clients’
fetchWithPlainHeadersto keep the upstream abort signal “in play” throughresponse.text()(including canceling the body stream on abort). - Add regression tests for both clients where
fetchresolves headers immediately but the response body never completes. - Add
.graphtrail/to.gitignore.
Note: I did not run ./scripts/verify in this review environment (no verification results to report).
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/adguard-client.ts |
Wraps fetch to extend abort/timeout coverage through response body consumption. |
src/adguard-sync-client.ts |
Same timeout/body-consumption protection for the Sync client transport. |
tests/client.test.ts |
Adds regression test for “headers resolved, body stalls” timeout behavior. |
tests/sync-client.test.ts |
Adds the analogous stalled-body timeout regression test for the Sync client. |
.gitignore |
Ignores .graphtrail/. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+175
to
+176
| const abortError = (): Error => | ||
| Object.assign(new Error("The operation was aborted"), { name: "AbortError" }); |
Comment on lines
+172
to
+173
| const abortError = (): Error => | ||
| Object.assign(new Error("The operation was aborted"), { name: "AbortError" }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fetchWithTimeout()ineffect-operator-kitclears its abort timer the momentfetch()resolves with response headers.response.text()then runs unguarded — a server that sends headers immediately but stalls the body stream bypassestimeoutMsentirely.Changes
fetchWithPlainHeaders(both clients) — converted from a thin header-normalization shim to a full round-trip wrapper: awaits headers, then racesresponse.text()against the sameAbortSignalpassed in viainit. If the timer fires mid-body, the body-read Promise rejects withAbortErrorimmediately, which the existingTransportErrorpath surfaces asAdGuardUnreachableError/AdGuardSyncUnreachableError.client.test.tsandsync-client.test.ts: stubfetchto resolve headers instantly but never close the body stream; assert thattimeoutMscauses the expected unreachable error rather than hanging indefinitely.