Skip to content

Don't swallow fetch TypeError as CORS in non-browser environments#1595

Open
bhosmer-ant wants to merge 2 commits intomainfrom
fix/bug-015-cors-retry-masks-errors
Open

Don't swallow fetch TypeError as CORS in non-browser environments#1595
bhosmer-ant wants to merge 2 commits intomainfrom
fix/bug-015-cors-retry-masks-errors

Conversation

@bhosmer-ant
Copy link
Contributor

fetchWithCorsRetry previously caught all TypeErrors from fetch() and either retried without headers or silently returned undefined. The intent was to work around CORS preflight failures triggered by the custom MCP-Protocol-Version header in browsers.

However, fetch() also throws TypeError for network failures (DNS resolution, connection refused, invalid URL). Swallowing those causes OAuth discovery to silently fall through to the next URL, masking the real error — users see a misleading "metadata not found" instead of the actual network failure.

CORS is a browser-only concept. In Node.js, Cloudflare Workers, and other non-browser runtimes, a TypeError from fetch is never a CORS error. This PR gates the CORS retry/swallow heuristic on running in a browser (detected via globalThis.document). In non-browser environments, TypeErrors now propagate immediately so callers see the underlying network failure.

In browsers, the existing behavior is preserved — we cannot reliably distinguish CORS TypeError from network TypeError from the error object alone, so the swallow-and-fallthrough heuristic still applies there.

Motivation and Context

The previous behavior made OAuth discovery failures in Node.js indistinguishable from "endpoint doesn't exist." A DNS outage, connection refused, or firewall block would all appear as if the auth server simply wasn't at that URL. This is bad for debuggability and could also lead callers to fall through to alternate discovery URLs when the intended server is temporarily unreachable.

How Has This Been Tested?

  • 9 existing CORS-retry tests now stub globalThis.document to simulate a browser environment (via withBrowserLikeEnvironment() helper, cleaned up in afterEach)
  • 2 new tests verify TypeError propagates immediately in non-browser environments without any CORS retry attempts

All 132 auth tests pass, 382 integration tests pass.

Breaking Changes

Behavioral change in non-browser environments: discoverOAuthProtectedResourceMetadata, discoverOAuthMetadata, and discoverAuthorizationServerMetadata will now throw TypeError on network failures instead of returning undefined. This surfaces real errors that were previously swallowed. Callers that were relying on the silent fallthrough behavior will now see the underlying network error.

In practice this is what users want — they want to know their auth server is unreachable, not silently fall through to a fallback URL.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Related to #545, #764, #1450 — users debugging auth failures get misleading errors because the underlying network problem is swallowed.

Known limitation: Web Workers and Service Workers run in browser contexts where CORS applies, but document is not defined there. With this change, a CORS error in a Worker would throw instead of falling through. OAuth discovery from within a Worker is an unusual use case, and the tradeoff seems right — Workers are more likely to hit genuine network errors than CORS issues with well-known OAuth endpoints.

fetchWithCorsRetry previously caught all TypeErrors from fetch() and
either retried without headers or silently returned undefined. The intent
was to work around CORS preflight failures triggered by the custom
MCP-Protocol-Version header in browsers.

However, fetch() also throws TypeError for network failures (DNS
resolution, connection refused, invalid URL). Swallowing those causes
OAuth discovery to silently fall through to the next URL, masking the
real error and giving users a misleading 'metadata not found' instead
of the actual network failure.

CORS is a browser-only concept. In Node.js, Workers, and other
non-browser runtimes, a TypeError from fetch is never a CORS error.
This change gates the CORS retry/swallow heuristic on running in a
browser (detected via globalThis.document). In non-browser environments,
TypeErrors now propagate immediately so callers see the underlying
network failure.

In browsers, the existing behavior is preserved: we cannot reliably
distinguish CORS TypeError from network TypeError from the error object
alone, so the swallow-and-fallthrough heuristic still applies there.

Tests that exercise CORS retry logic now stub globalThis.document to
simulate a browser environment.
Ensures the browser-environment stub is cleaned up even if a CORS test
throws an assertion error before reaching the explicit restore() call,
preventing the stubbed document from leaking into subsequent tests.
@bhosmer-ant bhosmer-ant requested a review from a team as a code owner February 26, 2026 20:15
@changeset-bot
Copy link

changeset-bot bot commented Feb 26, 2026

⚠️ No Changeset found

Latest commit: b8a2e61

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 26, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1595

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1595

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1595

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1595

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1595

commit: b8a2e61

@mattzcarey
Copy link
Contributor

mattzcarey commented Feb 27, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants