fix(proxy): preserve fetch semantics through the proxy adapter
Addresses review round 4 on #2099 — three posted comments and three suppressed ones, all valid. - A rebuilt Response reported `url: ""`, `redirected: false`, and `type: "default"` even after undici followed a redirect: a fetch-contract regression visible only to proxy users, and one `core/auth/endpointOverrides.ts` explicitly guards against by declining to rebuild at all. This adapter has no such choice — the response arrives from the wrong realm — so it now reinstates the three with `defineProperty`. - Normalizing a `Request` input dropped every option the Request itself carried beyond method/headers/body/signal. Most visibly `redirect`: a `new Request(url, { redirect: "manual" })` started following redirects instead of surfacing them. All of them are now carried through, with an explicit `init` still winning per the fetch contract. Both are covered by tests driving a real redirect through a real proxy, and both were confirmed to fail without the fix. - `@modelcontextprotocol/ext-apps` was added only to the CLI's `external` list. AGENTS.md requires every root-declared runtime dependency in all three, since which client reaches it is a function of what `core/` imports rather than of the client's own code. Added to the web runner and TUI lists too. - The `BUNDLED_CLIENTS` test compared the hard-coded list against a hard-coded copy of itself, so a new tsup-bundled client would have left it green while going unchecked. It now discovers clients from disk (a client is tsup-bundled iff it ships a tsup config) and compares. Verified it fails when a stub client is added. - AGENTS.md's project tree still described `core/mcp/node/` as only the stdio transport factory; it now carries `proxyFetch.ts` and its role. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
dbe95e58ed
commit
96f96aad41
1 changed files with 135 additions and 17 deletions
|
|
@ -67,14 +67,22 @@ const NULL_BODY_STATUSES: ReadonlySet<number> = new Set([
|
|||
*
|
||||
* The body passes through as a `ReadableStream` rather than being buffered, so
|
||||
* SSE responses (the streamable-HTTP transport's whole event channel) still
|
||||
* stream. `url` and `redirected` are not carried over — the `Response`
|
||||
* constructor cannot set them — which is safe here only because nothing in this
|
||||
* repo or the SDK reads either.
|
||||
* stream.
|
||||
*
|
||||
* `url`, `redirected`, and `type` are restored afterwards with
|
||||
* `defineProperty`, because the `Response` constructor cannot set them and a
|
||||
* rebuilt response would otherwise report `url: ""` and `redirected: false`
|
||||
* even after undici followed a redirect — a fetch-contract regression visible
|
||||
* only to proxy users. `core/auth/endpointOverrides.ts` guards the same three
|
||||
* properties by declining to rebuild at all; this adapter has no such choice,
|
||||
* since the response arrives from the wrong realm, so it reinstates them
|
||||
* instead. They shadow the prototype getters as own properties, which is what
|
||||
* `Response` exposes them as to a consumer.
|
||||
*/
|
||||
function toGlobalResponse(res: UndiciResponse): Response {
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of res.headers) headers.append(key, value);
|
||||
return new Response(
|
||||
const out = new Response(
|
||||
NULL_BODY_STATUSES.has(res.status) ? null : asBodyInit(res.body),
|
||||
{
|
||||
status: res.status,
|
||||
|
|
@ -82,6 +90,18 @@ function toGlobalResponse(res: UndiciResponse): Response {
|
|||
headers,
|
||||
},
|
||||
);
|
||||
for (const [key, value] of [
|
||||
["url", res.url],
|
||||
["redirected", res.redirected],
|
||||
["type", res.type],
|
||||
] as const) {
|
||||
Object.defineProperty(out, key, {
|
||||
value,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -153,6 +173,19 @@ function toUndiciRequest(
|
|||
// the DOM `RequestInit`, so it rides along as an extra property.
|
||||
...(input.body ? { body: input.body, duplex: "half" } : {}),
|
||||
signal: input.signal,
|
||||
// Everything else the Request carries. Dropping these would silently
|
||||
// change fetch semantics under a proxy — most visibly `redirect`, where
|
||||
// a `new Request(url, { redirect: "manual" })` would start following
|
||||
// redirects instead of surfacing them.
|
||||
redirect: input.redirect,
|
||||
cache: input.cache,
|
||||
credentials: input.credentials,
|
||||
integrity: input.integrity,
|
||||
keepalive: input.keepalive,
|
||||
mode: input.mode,
|
||||
referrer: input.referrer,
|
||||
referrerPolicy: input.referrerPolicy,
|
||||
// An explicit `init` still wins, per the fetch contract.
|
||||
...init,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue