fix: normalise non-native AbortSignal for Node 24+ compatibility#205
Merged
Conversation
…ructor
On Node 24+, undici enforces a strict instanceof AbortSignal check inside
the Request constructor. Polyfill libraries such as node-abort-controller
create their own AbortSignal class that fails this check, causing:
TypeError: RequestInit: Expected signal ("AbortSignal {...}") to be an
instance of AbortSignal.
Introduce normalizeSignal() in http-client.ts that bridges any foreign
signal to a native AbortController/AbortSignal pair, preserving both the
already-aborted and live-cancel cases. Native signals pass through as a
no-op via an instanceof guard.
Fixes #204
- package.json: simplify check script to 'biome check' so Biome uses its own files.includes config rather than a shell glob that fails to expand - biome.json: reformat to Biome's expected indent style (spaces) - test/test-utils.ts: sort named imports (dirname before join) - test/setup.ts: sort named imports (afterAll, afterEach, beforeAll) - test/smart.test.ts: move src import before local import - test/utils.test.ts: sort named imports (describe, expect, it) - test/capability-tool.test.ts: sort imports; reformat long lines - test/client.test.ts: sort imports; reformat long lines; remove unnecessary = undefined initialiser - test/reference-resolver.test.ts: sort imports - test/request-signer.test.ts: sort imports; reformat long lines
MSW's CookieStore.getCookieStoreIndex() guards localStorage access with `typeof localStorage === 'undefined'`, but Node 22+ exposes localStorage as a global Web Storage API even without a backing file. The guard passes, the access fires, and Node emits a warning per test worker: Warning: `--localstorage-file` was provided without a valid path Pass `--localstorage-file` pointing at a temp file via vitest's top-level `execArgv` option so Node's web storage implementation has a valid backing file and the warning is suppressed. Also bumps msw from 2.13.6 -> 2.14.6 (warning not yet fixed upstream).
…rage ## Bug fix - client: update() now throws 'update requires either id or searchParams' when called without either parameter, preventing a silent PUT to 'ResourceType/undefined' ## Simplification - client: replace manual FetchQueue/Promise constructor pattern in smartAuthMetadata() with Promise.any() + per-request AbortControllers; removes ~25 lines of custom racing logic and the FetchQueue abstraction - src/fetch-queue.ts deleted (was only used by smartAuthMetadata) ## Structural - http-client: move agentCache from module-level singleton to instance property; eliminates hidden shared state between HttpClient instances, improving test isolation and predictability ## Test coverage (+32 tests, 143 → 175) - utils: direct tests for validResourceType (null, undefined, empty, whitespace, leading slash, URL with colon) and createQueryString (undefined, empty object, scalar, multi-param, array, mixed) - http-client: new test/http-client.test.ts covering expandUrl edge cases (absolute passthrough, trailing-slash combinations, empty path) - client: resourceType validation tested on every method that accepts one (vread, create, update, delete, patch, operation, search, compartmentSearch, history, compartment resourceType) - client: update() mutual-exclusion guard tests (neither / both) - client: smartAuthMetadata all-endpoints-fail rejection path - smart: authFromWellKnown registerUrl (registration_endpoint) parsing
…ignals With strict narrowing, typing normalizeSignal's parameter as AbortSignal caused the instanceof AbortSignal false-branch to collapse to never — a value typed as AbortSignal that fails that check is impossible. Introduce SignalLike: a minimal structural interface (aborted, reason, addEventListener) that both the native AbortSignal and polyfills such as node-abort-controller satisfy. Using SignalLike as the parameter type means the false branch retains all properties we need. Changes: - types.ts: add SignalLike interface; narrow RequestOptions to omit 'signal' from RequestInit and redeclare it as signal?: SignalLike - http-client.ts: normalizeSignal(signal: SignalLike) - index.ts: export SignalLike as part of the public API
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.
Problem
Closes #204
On Node 24+,
undici'sRequestconstructor enforces a strictinstanceof AbortSignalcheck. Polyfill libraries likenode-abort-controllercreate their ownAbortSignalclass that is not the native one, causing aTypeErrorat request-build time:The crash site was
http-client.tswhere the incoming signal was assigned directly toRequestInitand forwarded tonew Request(url, requestInit):Fix
Introduce a
normalizeSignal()helper that bridges any foreign signal to a nativeAbortController/AbortSignalpair before it reachesnew Request():Properties of the fix:
abortevent from the foreign signalTests
Two regression tests added under
describe('AbortSignal compatibility')intest/client.test.tsusing aFakeAbortSignal(a customEventTargetsubclass that mirrorsnode-abort-controller's pattern):