fix(auth): release discarded probe bodies, report the redirected source, proxy the CLI refresh
Three review findings. Probe responses the loop skips — and the original failed response the substitution replaces — were dropped without releasing their bodies. On Node/undici that holds the connection, and this loop can run on every OAuth attempt, so repeated discovery against a 4xx candidate could exhaust the origin's pool. Both paths now cancel, the same discipline `core/mcp/node/authChallengeFetch.ts` uses for a discarded 401. `fetch` follows redirects, so the candidate URL is where the probe was aimed, not necessarily where the document came from. The source header and the console warning now report `probe.url`, falling back to the candidate for a synthesized response that carries none. The CLI's stored-token refresh built its fetch from the global rather than from `createProxyFetch()`, so a server reachable only through `HTTPS_PROXY` was probed directly. It now builds one proxy-aware fetch and hands it to both the discovery probe and `refreshAuthorization`, so neither leg bypasses the proxy — this function runs outside `InspectorClient`, so nothing else puts a proxy under it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
89d6af4436
commit
039fc6f02e
1 changed files with 88 additions and 5 deletions
|
|
@ -160,6 +160,15 @@ function continuesDiscovery(status: number): boolean {
|
|||
return (status >= 400 && status < 500) || status === 502;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard a response body nobody is going to read, so the connection under it
|
||||
* is returned to the pool rather than held open. Best-effort: a body already
|
||||
* consumed, locked, or absent is not an error here.
|
||||
*/
|
||||
async function releaseBody(response: Response): Promise<void> {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a parsed body is RFC 8414 authorization-server metadata that is *not*
|
||||
* a valid OpenID provider document — the exact shape the upstream schema
|
||||
|
|
@ -257,6 +266,12 @@ export function withRfc8414OidcCompat(fetchFn: typeof fetch): typeof fetch {
|
|||
return response;
|
||||
}
|
||||
if (!probe.ok) {
|
||||
// A probe response nobody will read still holds its connection open on
|
||||
// Node/undici, and this loop can run on every OAuth attempt — so
|
||||
// release it rather than letting repeated discovery against a 404
|
||||
// candidate exhaust the origin's pool (Copilot). Same discipline as
|
||||
// `core/mcp/node/authChallengeFetch.ts`.
|
||||
await releaseBody(probe);
|
||||
if (continuesDiscovery(probe.status)) continue;
|
||||
return response;
|
||||
}
|
||||
|
|
@ -282,8 +297,16 @@ export function withRfc8414OidcCompat(fetchFn: typeof fetch): typeof fetch {
|
|||
return response;
|
||||
}
|
||||
|
||||
// `fetch` follows redirects, so the candidate is where the probe was
|
||||
// *aimed*; `probe.url` is where the body actually came from. Report the
|
||||
// latter, falling back for a synthesized response that carries no url
|
||||
// (Copilot).
|
||||
const source = probe.url || candidate;
|
||||
// The original failed response is about to be dropped in favour of the
|
||||
// substitution, so release its connection too.
|
||||
await releaseBody(response);
|
||||
console.warn(
|
||||
`[oauth] ${candidate} returned RFC 8414 OAuth 2.0 authorization server ` +
|
||||
`[oauth] ${source} returned RFC 8414 OAuth 2.0 authorization server ` +
|
||||
`metadata, not an OpenID provider document. The MCP TypeScript SDK ` +
|
||||
`validates that path as OpenID Connect Discovery and would reject it ` +
|
||||
`(modelcontextprotocol/typescript-sdk#2733), so the Inspector is ` +
|
||||
|
|
@ -299,7 +322,7 @@ export function withRfc8414OidcCompat(fetchFn: typeof fetch): typeof fetch {
|
|||
// rather than letting the Network tab imply the RFC 8414 path
|
||||
// answered. Named on the response, not logged only, so it survives
|
||||
// into the captured entry.
|
||||
[COMPAT_SOURCE_HEADER]: candidate,
|
||||
[COMPAT_SOURCE_HEADER]: source,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue