Web HTTP Adapter¶
Spec topic: web dashboard HTTP layer.
Decision¶
The dashboard uses axios with the default XMLHttpRequest adapter
in production and in tests. MSW 2.x intercepts requests in the test
environment via its XHR interceptor.
Test correctness with respect to forgotten timers, sockets, and
listeners is enforced at runtime by the per-test active-handle
detector documented in
web-active-handle-detection.md.
That detector replaces the previous vitest --detect-async-leaks
ceiling: instead of counting Promises (most of which originate in
node_modules/), it diffs the live set of Node async_hooks
resources around every test and fails the run when a tracked handle
attributable to web/src/ survives afterEach.
What the active-handle gate enforces here¶
The HTTP adapter is one of the surfaces the previous --detect-async-leaks
count exercised most heavily, because every MSW-intercepted request
allocates Promises inside @mswjs/interceptors, axios's internal
chain, and (in jsdom) tough-cookie's sync-shaped Promise wrapper.
Those Promises always settle during the test; they are not real
leaks. The active-handle detector ignores them entirely because
Promises are not event-loop-holding resources. What it does enforce
is: a request that schedules a timer (e.g. a retry backoff via
setTimeout) MUST cancel that timer before its test ends, or the
test fails.
What's in the codebase¶
- Cookie shim:
web/src/cookie-shim.ts, installed fromweb/src/test-setup.tsxandweb/src/bench-setup.ts. ReplacesDocument.prototype'scookiedescriptor with a synchronous in-memory jar so jsdom's tough-cookie path does not allocate per-write Promises during tests. Test-speed optimisation; prototype-pollution defence is the primary security purpose. - Storage shim:
web/src/storage-shim.ts, installed fromweb/src/test-setup.tsx. PatchesStorage.prototypemethods to bypass jsdom's_dispatchStorageEventsetTimeout(0)path; no app or test code subscribes to thestorageevent, so the dispatch is dead weight in the test runner. Backed by an instance-keyedWeakMapsolocalStorage instanceof Storagestays true andvi.spyOn(Storage.prototype, 'setItem')continues to intercept. - Synchronous request interceptor:
web/src/api/client.tspasses{ synchronous: true }toapiClient.interceptors.request.use(...)so axios skips the.then(chain[i++], ...)loop atAxios.js:196when no async interceptor is registered. The CSRF interceptor is itself synchronous; the annotation tells axios to take the fast path. No behavioural change; pure perf hint.
Behavioural audit (axios fetch adapter)¶
axios.defaults.adapter defaults to the XHR adapter in browsers and
ships with a fetch adapter alternative. The fetch adapter is not
adopted because:
| Dimension | axios XHR (current prod) | axios fetch (evaluated) | Verdict |
|---|---|---|---|
responseType: 'blob' |
Works (src/api/endpoints/artifacts.ts:30) |
Supported in axios 1.15+ but the conversion path differs; untested against downloadArtifactContent |
No change needed |
responseType: 'text' |
Works (artifacts.ts:38) |
Same as above | No change needed |
withCredentials: true |
Works (src/api/client.ts:52); cookies attached automatically |
credentials: 'include' equivalent; cookie handling differs between browsers and jsdom but is identical in prod browsers |
No behavioural delta in prod |
timeout: 30_000 |
xhr.timeout (wire time) |
AbortSignal.timeout (wall-clock from request start) |
Prod-browser behaviour equivalent |
429 retry + Retry-After + _rateLimitRetries |
Works; config mutation survives axios's recursive apiClient.request(retryConfig) |
Fetch adapter clones config differently; would need WeakMap<InternalAxiosRequestConfig, number> refactor to be safe |
Non-trivial refactor required |
CSRF interceptor (client.ts:110-119) |
Works | Same; headers attachment is adapter-agnostic | No change |
401 handler (client.ts:128-140) |
Works | Same | No change |
ApiResponse / PaginatedResponse envelope unwrap |
Works | Same; response.data shape is adapter-agnostic |
No change |
signal: AbortController |
Works | Works | No change |
SSE / streaming (src/api/endpoints/providers.ts:162-237) |
Already uses native fetch directly, bypassing axios |
Unchanged | Adapter-agnostic |
onUploadProgress / onDownloadProgress |
Not used anywhere | Fetch adapter does not support upload progress | N/A |
FormData / File / Blob bodies |
Not used | Both adapters support natively | N/A |
paramsSerializer, maxContentLength, decompress |
Not configured | Same | No change |
err.request introspection |
Not used in app or tests | Fetch adapter does not set response.request |
No change |
| Bundle size | Tree-shaken into vendor-state chunk (~5KB gzipped contribution) |
Similar size | Not a forcing factor |
| Browser support | All modern (no IE11 constraint) | All modern | Equivalent |
| MSW interception | Via @mswjs/interceptors/XMLHttpRequest |
Via @mswjs/interceptors/fetch |
Functionally equivalent for tests under the active-handle gate |
There is no operational reason to move production to the fetch
adapter. The fetch adapter would require the _rateLimitRetries
WeakMap refactor to keep the retry path safe, with no countervailing
production benefit.
References¶
- web-active-handle-detection.md: what the per-test handle gate catches, allowlist policy, and telemetry shape.
web/CLAUDE.md: MSW handlers contract and test teardown rules.docs/reference/web-zustand-stores.md: per-store teardown catalog.