-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix: continue OAuth metadata discovery on 5xx responses #1632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
3d98845
b5b0360
bdcc107
c9a6012
973f787
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@modelcontextprotocol/client': patch | ||
| --- | ||
|
|
||
| Continue OAuth metadata discovery on 5xx responses instead of throwing, matching the existing behavior for 4xx. This fixes MCP servers behind reverse proxies that return 502/503 for path-aware metadata URLs. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1001,8 +1001,8 @@ | |
| /** | ||
| * Determines if fallback to root discovery should be attempted | ||
| */ | ||
| function shouldAttemptFallback(response: Response | undefined, pathname: string): boolean { | ||
| return !response || (response.status >= 400 && response.status < 500 && pathname !== '/'); | ||
| return !response || (!response.ok && pathname !== '/'); | ||
|
Check warning on line 1005 in packages/client/src/client/auth.ts
|
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1028,11 +1028,11 @@ | |
| } | ||
|
|
||
| let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn); | ||
|
|
||
| // If path-aware discovery fails with 404 and we're not already at root, try fallback to root discovery | ||
| // If path-aware discovery fails (any non-OK status) and we're not already at root, try fallback to root discovery | ||
| if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) { | ||
| const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer); | ||
| response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn); | ||
|
Check notice on line 1035 in packages/client/src/client/auth.ts
|
||
|
Comment on lines
1066
to
1070
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 Nit: In Extended reasoning...When let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) {
const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
}This is inconsistent with Concrete example: An MCP server at Unconsumed response bodies can prevent HTTP/1.1 connection reuse because the connection cannot be returned to the pool until the body stream is fully read or the connection is closed. In practice, the impact is minimal here: this is a one-time discovery call (not a hot path), well-known endpoints typically return small responses, and modern Node.js runtimes (undici) will eventually GC unconsumed bodies. Still, it is an inconsistency worth fixing for correctness. This is a pre-existing issue. Before this PR, the same code path was triggered for 4xx responses (the old condition was |
||
| } | ||
|
|
||
| return response; | ||
|
|
@@ -1197,13 +1197,7 @@ | |
|
|
||
| if (!response.ok) { | ||
| await response.text?.().catch(() => {}); | ||
| // Continue looking for any 4xx response code. | ||
| if (response.status >= 400 && response.status < 500) { | ||
| continue; // Try next URL | ||
| } | ||
| throw new Error( | ||
| `HTTP ${response.status} trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}` | ||
| ); | ||
| continue; // Try next URL for any non-OK response (4xx, 5xx) | ||
| } | ||
|
|
||
| // Parse and validate based on type | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The JSDoc for the deprecated
discoverOAuthMetadata()(lines 1044-1045) says "Any other errors will be thrown as exceptions," but after broadeningshouldAttemptFallbackto!response.ok, a 5xx on a path-aware URL now triggers root fallback — and if the root returns 404, the function silently returnsundefinedinstead of throwing. The doc should note that non-OK responses on path-aware URLs may trigger fallback before throwing.Extended reasoning...
The
shouldAttemptFallback()function was changed fromresponse.status >= 400 && response.status < 500to!response.ok, broadening whendiscoverMetadataWithFallback()attempts root fallback. This creates a new code path in the callerdiscoverOAuthMetadata()where the JSDoc becomes inaccurate.The JSDoc at lines 1044-1045 states: "If the server returns a 404 for the well-known endpoint, this function will return
undefined. Any other errors will be thrown as exceptions." This implies that non-404 errors (including 5xx) will always throw. However, this is no longer true for path-aware URLs.Step-by-step proof: Consider calling
discoverOAuthMetadata("https://auth.example.com/tenant1", { authorizationServerUrl: "https://auth.example.com/tenant1" }). (1)discoverMetadataWithFallbackconstructs the path-aware well-known URL and callstryMetadataDiscovery, which returns a 502 response. (2)shouldAttemptFallback(response, "/tenant1")evaluates!response || (!response.ok && pathname !== "/")→!false || (!false && true)→true. (3) Fallback to root URL/.well-known/oauth-authorization-serveris attempted, which returns 404. (4) Back indiscoverOAuthMetadata, the response is now the 404 from root. (5) The check!response || response.status === 404is true, so the function returnsundefined. Pre-PR, step 2 would have evaluatedresponse.status >= 400 && response.status < 500→falsefor 502, so no fallback would occur, and the function would hit!response.ok→throw new Error("HTTP 502 ...").The impact is minimal:
discoverOAuthMetadatais deprecated in favor ofdiscoverAuthorizationServerMetadata, and the behavioral change (falling back on 5xx) is the intended purpose of this PR. The issue is purely that the documentation was not updated to reflect the new behavior.The fix is straightforward: update the JSDoc to note that for path-aware URLs, non-OK responses (including 5xx) trigger a fallback to root discovery before throwing, and the function may return
undefinedif the root also fails with 404.