fix: make the proxy deadline per-request, not unconditional (#2319 round 13)
The server-side timer added in round 9 applied to every /api/fetch request. But that route carries MCP traffic as well as OAuth work, so a Streamable HTTP tool call that legitimately withholds its response headers past the budget was aborted there — reintroducing on the backend exactly the regression exemptMcpEndpoint prevents on the client, and reporting it as an OAuth timeout besides. The caller now decides. `withOAuthRequestTimeout` stamps its budget on the RequestInit it hands down, `createRemoteFetch` reads it back and puts it in the /api/fetch envelope, and the route applies a timer only when one is present — so an exempt MCP request, which never gets an init of its own, is bounded by nothing. The carrier is a WeakMap keyed on the init rather than a header: serializeRequest copies request headers verbatim into the payload and the route re-sends them to the authorization server, so a marker header would leak to a third party. It needs no cast onto RequestInit and is collected with the object. The `fetchTimeoutMs` option from round 9 is gone — the deadline now lives in exactly one place, the client wrapper, which is also what makes the 504 marker honest: only a request the OAuth wrapper bounded can produce one. Ten tests: the stamping and its rounding, nothing stamped on an exempt call or an init that never went through the wrapper, the envelope carrying the budget (and not a header), its omission on both exempt and direct calls, and at the route the 504 marker, no deadline without one, and a non-numeric deadline ignored. The no-deadline case is verified by mutation: an unconditional timer makes it fail. 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>
This commit is contained in:
parent
234d0a9ac4
commit
de6960ec5a
1 changed files with 259 additions and 50 deletions
|
|
@ -22,6 +22,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
deadlineForRequestInit,
|
||||
isOAuthRequestTimeoutWire,
|
||||
OAuthRequestTimeoutError,
|
||||
} from "../../auth/requestTimeout.js";
|
||||
|
|
@ -154,10 +155,26 @@ export function createRemoteFetch(options: RemoteFetchOptions): typeof fetch {
|
|||
? input.signal
|
||||
: undefined;
|
||||
|
||||
// #2319: tell the route whether *this* request is bounded, and by how much.
|
||||
// The route serves MCP traffic as well as OAuth work, and a Streamable HTTP
|
||||
// tool call can legitimately withhold its response headers for minutes — so
|
||||
// a timer there must be per-request rather than unconditional (Copilot). An
|
||||
// exempt request carries no deadline and the route applies none.
|
||||
//
|
||||
// In the JSON envelope rather than a header: `headers` is re-sent verbatim
|
||||
// to the upstream server, so a marker header would leak to a third party.
|
||||
const timeoutMs = deadlineForRequestInit(init);
|
||||
|
||||
const res = await fetchFn(`${baseUrl}/api/fetch`, {
|
||||
method: "POST",
|
||||
headers: reqHeaders,
|
||||
body: JSON.stringify({ url, method, headers, body }),
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
...(timeoutMs !== undefined && { timeoutMs }),
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue