Skip to content

Commit 113c83f

Browse files
committed
Keep following a relative redirect when the endpoint URL carries userinfo
A relative Location joined onto a URL with user:pass@ keeps that userinfo, which is the caller's own credential for the same origin; only refuse userinfo the redirect itself introduces.
1 parent 0cff7e3 commit 113c83f

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

src/mcp/shared/_httpx_utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,18 @@ def next_request_within_origin(response: httpx.Response) -> httpx.Request | None
9191
GET: httpx turns a POST into a body-less GET for 301/302/303, which would
9292
drop the message), its URL stays within the origin of the request just sent
9393
(same scheme, host and port, or http to https on the same host with default
94-
ports), and the Location carries no userinfo (which httpx would otherwise
95-
send as Basic auth). None for anything else, including a non-redirect.
94+
ports), and the Location does not bring userinfo of its own (which httpx
95+
would otherwise send as Basic auth; userinfo the configured URL already had
96+
is kept by a relative Location and is fine). None for anything else,
97+
including a non-redirect.
9698
"""
9799
next_request = response.next_request
98100
if next_request is None:
99101
return None
100102
sent = response.request
101103
if (
102104
next_request.method != sent.method
103-
or next_request.url.userinfo
105+
or (next_request.url.userinfo and next_request.url.userinfo != sent.url.userinfo)
104106
or not _within_origin(sent.url, next_request.url)
105107
):
106108
return None

tests/shared/test_httpx_utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ async def test_redirect_location_with_userinfo_is_not_followed():
208208
assert received == [f"POST {url}"]
209209

210210

211+
async def test_userinfo_of_the_configured_url_kept_by_a_relative_location_is_followed():
212+
"""Userinfo the caller put in the endpoint URL is carried over by a relative Location (URL join
213+
keeps the authority); that is the caller's own credential for the same origin, so the redirect
214+
is followed as httpx itself would (SDK-defined)."""
215+
url = "http://user:secret@mcp.example/mcp"
216+
client, received, _ = _recording_client({url: (307, "/mcp/")})
217+
218+
async with client, stream_within_origin(client, "POST", url, content=b"payload") as response:
219+
await response.aread()
220+
221+
assert response.status_code == 200
222+
assert received == [f"POST {url}", "POST http://user:secret@mcp.example/mcp/"]
223+
224+
211225
async def test_request_within_origin_returns_a_read_response():
212226
"""The non-streaming form hands back a response whose body is already read."""
213227
url = "http://mcp.example/mcp"

0 commit comments

Comments
 (0)