feat(client): send a same-origin Origin header by default (streamable HTTP)#2782
Open
fede-kamel wants to merge 1 commit into
Open
feat(client): send a same-origin Origin header by default (streamable HTTP)#2782fede-kamel wants to merge 1 commit into
fede-kamel wants to merge 1 commit into
Conversation
… HTTP) The streamable HTTP client sends no Origin header. Browsers always send one on cross-origin-capable requests; emitting a correct same-origin value matches that behavior and satisfies servers that gate state-changing requests on a present, same-origin Origin (defense-in-depth against DNS-rebinding / CSRF), without weakening any server's posture. The value is derived from httpx.URL so it uses the exact scheme/host/port normalization httpx applies to the Host header (default ports dropped, IPv6 hosts bracketed, userinfo stripped). Origin and Host therefore stay byte-for-byte consistent even for inputs like https://host:443/mcp, where naive string parsing keeps a redundant :443 that would not match the Host httpx sends. A caller-provided Origin always wins, and the caller's httpx client headers are never mutated. Refs modelcontextprotocol#2727
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The streamable HTTP client opens its handshake with no
Originheader. This PR makes it send a same-originOriginderived from the target URL by default, matching browser behavior and satisfying servers that gate state-changing requests on a present, same-originOrigin(defense-in-depth against DNS-rebinding / CSRF) — without weakening any server's posture.Refs #2727.
Honest scoping (please read)
While preparing this I verified the motivating claim in #2727 — that go-sdk's
http.CrossOriginProtectionrejects the Python client with403because it sends noOrigin. It does not. I built a Go 1.25 server with the realhttp.NewCrossOriginProtection()(the exact type go-sdk calls) and tested it:Origin(client today)200— acceptedOriginmismatchingHost403OriginmatchingHost(this PR)200The stdlib
Check()hasif origin == "" { return nil }— it fails open for non-browser clients. So this change is not a fix for that403; I don't want to oversell it. It's worthwhile as browser-parity + defense-in-depth, and for interop with any server that does require a present same-originOrigin. If maintainers feel the client should keep sending nothing, that's a reasonable call and I'll close this.Why derive from
httpx.URL(the one real correctness point)The server compares the
Originhost against theHostheader. Building the origin fromhttpx.URLreuses the exact normalization httpx applies toHost(default ports dropped, IPv6 bracketed, userinfo stripped), so the two always agree. This matters for an explicit default port:Hosthttpx sendsurlsplitoriginhttpx.URLorigin (this PR)https://h:443/mcphhttps://h:443403https://hSo a careless derivation can turn a working
200into a403; this one can't.Behavior
Origin(on theirhttpx.AsyncClient) always wins; the default only fills the gap.Tests & checks
test_get_default_origin_normalizes_authority/..._returns_none_without_web_origin— derivation incl. default-port drop, IPv6, userinfo, non-HTTP.test_streamable_http_client_sends_same_origin_by_default— in-process (Run StreamableHTTP transport tests in process instead of over sockets #2767 harness); asserts the sentOriginequals theHostand the caller's client is untouched.test_streamable_http_client_preserves_custom_origin— callerOriginwins../scripts/test: full suite green, 100.00% branch coverage,strict-no-coverclean;ruff+pyrightclean.Relationship to existing PRs
#2729, #2731, #2752, #2759 already propose an Origin header for #2727. This one differs by deriving from
httpx.URLso theOrigin/Hostmatch holds for explicit default ports (the cases theurlsplit-based versions get wrong). Happy to consolidate into whichever the maintainers prefer rather than add a fifth — flagging so this isn't duplicate noise.