Commit graph inspector/core/auth/requestTimeout.ts
Author SHA1 Message Date
cliffhall
e89ea4cf19 fix: address Copilot review round 1 on #2319
Three findings, all real:

- The deadline stopped at the response headers. `fetch` resolves once
  headers arrive, so a server that sent headers — or half a JSON
  document — and then stalled left the caller's `response.json()`
  hanging with nothing watching it. The body is now buffered under the
  same race and the response rebuilt around it, preserving `url`,
  `redirected` and `type`, which have no constructor option, and
  passing `null` for a null-body status.

- The caller's signal was only forwarded, not raced. On the browser's
  `createRemoteFetch` path the signal is dropped in flight, so an abort
  left the promise pending for the whole budget rather than the outer
  cancellation winning as documented. It is raced now, with the
  caller's own `reason` preserved, and the listener removed in
  `finally` since one caller signal outlives several requests.

- A `Request` carries its signal on the input, not in `init`, so always
  passing our own `signal` in `init` silently overrode it. The caller
  signal is now derived from the `Request` when `init` has no `signal`
  key — and an explicit `null` there still wins, per the fetch spec.

Eight new tests cover each: the stalled body, caller cancellation
against a signal-ignoring fetch, an already-aborted signal, the
Request-embedded signal, the explicit-null override, listener removal,
the rebuilt response's metadata and a null-body status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-11 02:24:30 -04:00
cliffhall
8dd7bc2920 feat: bound every OAuth-path request with a deadline
Protected-resource-metadata discovery, authorization-server metadata
discovery, dynamic client registration, the token exchange and the
refresh all went out with no AbortSignal at all. Token revocation was
the single exception, and its withDeadline race is the pattern this
generalizes.

Adds core/auth/requestTimeout.ts: withOAuthRequestTimeout wraps a fetch
so every call through it is bounded, composing any caller signal with
AbortSignal.any and racing a timer so the deadline holds even where the
signal is dropped in flight (the browser's remote fetch re-issues the
call to /api/fetch and forwards no signal). The rejection is an
OAuthRequestTimeoutError naming the URL, so a stall says which endpoint
stalled rather than riding the SDK handshake timeout and reporting
"Request timed out".

Applied inside InspectorClient.buildEffectiveAuthFetch rather than to
this.fetchFn, because the transport is handed this.fetchFn directly and
a deadline on a Streamable HTTP / SSE response would sever the stream it
is supposed to hold open. Also applied to the CLI's storedAuthFetch,
which calls SDK discovery directly and so has nothing else bounding it.

Closes #2319

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFnM6RqB4nXqNysgUYmjmq
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-11 00:45:04 -04:00