Skip to content

Scope HTTP client redirect following to the request's origin#3075

Draft
maxisbey wants to merge 1 commit into
mainfrom
scope-headers-to-origin
Draft

Scope HTTP client redirect following to the request's origin#3075
maxisbey wants to merge 1 commit into
mainfrom
scope-headers-to-origin

Conversation

@maxisbey

@maxisbey maxisbey commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Clients created by the SDK's httpx factory always enabled follow_redirects=True, so everything configured on a client — headers, auth, request bodies — was re-sent to whatever host a Location header named, and a misconfigured URL failed with an unhelpful Unexpected content type: error. This PR scopes redirect following to the request's origin and makes every refused redirect fail loudly with the target URL and the remedy.

Motivation and Context

follow_redirects=True was added for operational convenience: trailing-slash 307s from Starlette-mounted servers (#105, #732) and hosted-server redirects (#283). The dominant case — the same-origin slash fix — keeps working exactly as before; the server-side cause was also fixed long ago in #1115. What the blanket default additionally did was silently re-send client configuration to arbitrary redirect targets and silently mask misconfigured URLs. httpx's own default is follow_redirects=False for the same reason (encode/httpx#2533 keeps its cross-origin strip list deliberately minimal and leaves per-application policy to the application).

Clients built by create_mcp_http_client now:

  • follow redirects within the same origin (scheme, host, port) and same-host http→https upgrades on default ports — trailing-slash normalization and TLS-upgrade redirects keep working, verified end-to-end against real Starlette apps;
  • raise the new RedirectError for any other redirect target, before the next request is sent, with a message naming the resolved target and how to proceed (connect to the final URL, or supply your own pre-configured httpx.AsyncClient after verifying the destination).

Transports resolve a refused redirect in-band — a request gets a JSON-RPC error rather than a hung caller, notifications are delivered to the session's message handler, and the standalone GET stream stops retrying an endpoint that redirects on every attempt. Caller-supplied clients that follow no redirects (httpx's default) get the same clear error on POST, GET-stream, and SSE-connect paths. OAuth discovery, registration, token, refresh, and the identity-assertion token exchange fail loudly on redirect responses instead of silently trying the next URL or abandoning the discovery chain.

create_mcp_http_client and RedirectError are now exported from the top-level mcp package (a deliberate API addition): the docs previously told users to build their own httpx.AsyncClient for headers/auth, which quietly lost the SDK's defaults. Docs and examples now configure clients through the factory; the general-purpose simple-tool fetch example gets a browser-like client of its own, since it fetches arbitrary web pages rather than a configured MCP endpoint.

How Has This Been Tested?

  • Real-server verification: booted MCPServer.streamable_http_app() under uvicorn and drove it with the real client. A trailing-slash /mcp/ URL initializes and completes a full session lifecycle (every request rides a genuine Starlette 307); a redirect to a different origin resolves initialization with a clear error naming the target; a typo'd path still yields the usual not-found error.
  • New unit and transport tests (all proven failing on the base revision): same-origin and https-upgrade redirects followed with method/body/headers intact; other-host, other-port, https→http, and empty-host absolute-form Locations refused before a second request is sent; unparsable Location defers to httpx's own error; per-request and client-level headers behave identically; request/notification/resumption/GET-stream/SSE paths each surface the in-band error; OAuth handlers raise on redirect responses; a caller-supplied follow_redirects=True client is untouched (opt-in preserved).
  • A parametrized test asserts the target-resolution helper matches response.next_request.url from httpx itself across relative, protocol-relative, absolute-form-without-host, and cross-origin shapes, so an httpx behavior change surfaces as a test failure rather than a silent policy divergence.
  • Full suite: 5458 passed, 100% coverage including strict-no-cover, pyright and ruff clean.

Breaking Changes

Yes, deliberate and documented in docs/migration.md: a redirect to a different origin now raises mcp.RedirectError instead of being followed. Same-origin redirects and same-host https upgrades are unaffected. Deployments that genuinely need cross-origin redirect following can pass their own httpx.AsyncClient(follow_redirects=True) to streamable_http_client / sse_client.

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

  • Four # pragma: no branch / # pragma: no cover comments are added in tests: three for the coverage.py nested-async with phantom-arc misreport documented in AGENTS.md, one marking an unreachable context-manager body in a connect-failure test.
  • Known, accepted scope edges: resumption/reconnection GETs with a caller-supplied non-following client surface httpx's generic status error rather than the typed one (the factory's clients get the typed error everywhere); the origin comparison is deliberately conservative and fails loud on exotic-but-equivalent forms (IPv6 long-form vs. shorthand, trailing-dot hosts, non-default-port https upgrades) — supply your own client for those chains.
  • The RedirectPolicy proposal in fix: add SSRF redirect protection to httpx client factory #2180 addressed the scheme-downgrade slice of the same default; with redirect targets now vetted at the factory for every consumer, that policy knob is superseded by this change.

AI Disclaimer

create_mcp_http_client followed every redirect, so everything configured
on a client (headers, auth, request bodies) was re-sent to whatever host
a Location header named. Clients built by the factory now follow
redirects within the same origin (scheme, host, and port), plus
http-to-https upgrades of the same host on default ports, and raise the
new RedirectError for anything else - before the next request is sent.

- transports resolve a refused redirect in-band: requests get a JSON-RPC
  error naming the target and the remedy, notifications are delivered to
  the session's message handler; the standalone GET stream stops
  retrying an endpoint that keeps redirecting
- caller-supplied clients that follow no redirects get the same clear
  error on POST, GET stream, and SSE connect instead of an opaque
  content-type error
- OAuth discovery, registration, token, refresh, and the
  identity-assertion token exchange fail loudly on redirect responses
  instead of silently trying the next URL or abandoning the discovery
  chain
- RedirectError and create_mcp_http_client are exported from the
  top-level mcp package; migration.md documents the behavior change;
  docs and examples configure clients through the factory, and the
  general-purpose fetch example uses a browser-like client of its own
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3075.mcp-python-docs.pages.dev
Deployment https://2e0d3ec4.mcp-python-docs.pages.dev
Commit b8a107d
Triggered by @maxisbey
Updated 2026-07-08 10:24:13 UTC

@maxisbey maxisbey requested a review from pcarleton July 8, 2026 10:28
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.

1 participant