@@ -66,11 +66,12 @@ async def __aexit__(self, exc_type, exc, tb):
6666 return False
6767
6868 class FakeClient :
69- def stream (self , method , url , * , content , headers , timeout ): # noqa: ANN001
69+ def stream (self , method , url , * , content , headers , timeout , follow_redirects ): # noqa: ANN001
7070 captured ["method" ] = method
7171 captured ["url" ] = url
7272 captured ["headers" ] = headers
7373 captured ["timeout" ] = timeout
74+ captured ["follow_redirects" ] = follow_redirects
7475 return FakeStreamContext (content = content )
7576
7677 monkeypatch .setattr ("mcpgateway.transports.rust_mcp_runtime_proxy.settings.experimental_rust_mcp_runtime_url" , "http://127.0.0.1:8787" )
@@ -125,6 +126,7 @@ async def send(message):
125126 assert captured ["method" ] == "POST"
126127 assert captured ["url" ] == "http://127.0.0.1:8787/mcp/?session_id=abc123"
127128 assert captured ["timeout" ].connect == 17
129+ assert captured ["follow_redirects" ] is False
128130
129131 forwarded_headers = dict (captured ["headers" ])
130132 assert forwarded_headers ["authorization" ] == "Bearer test-token"
@@ -188,11 +190,12 @@ async def __aexit__(self, exc_type, exc, tb):
188190 return False
189191
190192 class FakeClient :
191- def stream (self , method , url , * , content , headers , timeout ): # noqa: ANN001
193+ def stream (self , method , url , * , content , headers , timeout , follow_redirects ): # noqa: ANN001
192194 captured ["method" ] = method
193195 captured ["url" ] = url
194196 captured ["headers" ] = headers
195197 captured ["timeout" ] = timeout
198+ captured ["follow_redirects" ] = follow_redirects
196199 return FakeStreamContext (content = content )
197200
198201 async def receive ():
@@ -233,6 +236,7 @@ async def send(message):
233236 assert captured ["method" ] == "POST"
234237 assert captured ["url" ] == "http://127.0.0.1:8787/mcp/"
235238 assert captured ["content" ] == b'{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}'
239+ assert captured ["follow_redirects" ] is False
236240 assert events [- 1 ] == {"type" : "http.response.body" , "body" : b"" , "more_body" : False }
237241
238242
@@ -257,11 +261,12 @@ async def __aexit__(self, exc_type, exc, tb):
257261 return False
258262
259263 class FakeClient :
260- def stream (self , method , url , * , content , headers , timeout ): # noqa: ANN001
264+ def stream (self , method , url , * , content , headers , timeout , follow_redirects ): # noqa: ANN001
261265 captured ["method" ] = method
262266 captured ["url" ] = url
263267 captured ["headers" ] = dict (headers )
264268 captured ["timeout" ] = timeout
269+ captured ["follow_redirects" ] = follow_redirects
265270 return FakeStreamContext ()
266271
267272 monkeypatch .setattr ("mcpgateway.transports.rust_mcp_runtime_proxy.settings.experimental_rust_mcp_runtime_url" , "http://127.0.0.1:8787" )
@@ -298,6 +303,7 @@ async def send(message):
298303
299304 assert captured ["method" ] == "GET"
300305 assert captured ["url" ] == "http://127.0.0.1:8787/mcp/?session_id=session-1"
306+ assert captured ["follow_redirects" ] is False
301307 assert captured ["headers" ]["x-contextforge-affinity-forwarded" ] == "rust"
302308 assert "x-forwarded-internally" not in captured ["headers" ]
303309 assert "x-original-worker" not in captured ["headers" ]
@@ -332,12 +338,13 @@ async def __aexit__(self, exc_type, exc, tb):
332338 return False
333339
334340 class FakeClient :
335- def stream (self , method , url , * , content , headers , timeout ): # noqa: ANN001
341+ def stream (self , method , url , * , content , headers , timeout , follow_redirects ): # noqa: ANN001
336342 captured ["method" ] = method
337343 captured ["url" ] = url
338344 captured ["content" ] = content
339345 captured ["headers" ] = headers
340346 captured ["timeout" ] = timeout
347+ captured ["follow_redirects" ] = follow_redirects
341348 return FakeStreamContext ()
342349
343350 monkeypatch .setattr ("mcpgateway.transports.rust_mcp_runtime_proxy.settings.experimental_rust_mcp_runtime_url" , "http://127.0.0.1:8787" )
@@ -371,6 +378,7 @@ async def send(message):
371378 assert captured ["method" ] == "GET"
372379 assert captured ["url" ] == "http://127.0.0.1:8787/mcp/?session_id=abc123"
373380 assert captured ["content" ] == b""
381+ assert captured ["follow_redirects" ] is False
374382 assert dict (captured ["headers" ])["x-contextforge-server-id" ] == "123e4567-e89b-12d3-a456-426614174000"
375383 assert events [0 ]["status" ] == 200
376384 assert (b"content-type" , b"text/event-stream" ) in events [0 ]["headers" ]
@@ -402,11 +410,12 @@ class FakeAsyncClient:
402410 def __init__ (self , ** kwargs ):
403411 constructed ["kwargs" ] = kwargs
404412
405- def stream (self , method , url , * , content , headers , timeout ): # noqa: ANN001
413+ def stream (self , method , url , * , content , headers , timeout , follow_redirects ): # noqa: ANN001
406414 constructed ["method" ] = method
407415 constructed ["url" ] = url
408416 constructed ["headers" ] = headers
409417 constructed ["timeout" ] = timeout
418+ constructed ["follow_redirects" ] = follow_redirects
410419 return FakeStreamContext ()
411420
412421 get_http_client_mock = AsyncMock ()
@@ -441,6 +450,57 @@ async def send(message):
441450 assert constructed ["method" ] == "POST"
442451 assert constructed ["url" ] == "http://localhost/mcp/"
443452 assert constructed ["kwargs" ]["transport" ]._pool ._uds == "/tmp/contextforge-mcp-rust.sock" # pylint: disable=protected-access
453+ assert constructed ["kwargs" ]["follow_redirects" ] is False
454+ assert constructed ["follow_redirects" ] is False
455+ assert events [- 1 ] == {"type" : "http.response.body" , "body" : b"" , "more_body" : False }
456+
457+
458+ @pytest .mark .asyncio
459+ async def test_runtime_proxy_surfaces_redirect_without_following (monkeypatch ):
460+ """Internal runtime redirects should be surfaced directly and never auto-followed."""
461+ requests_seen = []
462+
463+ def handler (request : httpx .Request ) -> httpx .Response :
464+ requests_seen .append (str (request .url ))
465+ if request .url .path == "/mcp/" :
466+ return httpx .Response (
467+ 307 ,
468+ headers = {"location" : "http://127.0.0.1:8787/final" },
469+ request = request ,
470+ )
471+ return httpx .Response (200 , json = {"jsonrpc" : "2.0" , "id" : 1 , "result" : {"unexpected" : True }}, request = request )
472+
473+ client = httpx .AsyncClient (transport = httpx .MockTransport (handler ))
474+ monkeypatch .setattr ("mcpgateway.transports.rust_mcp_runtime_proxy.settings.experimental_rust_mcp_runtime_url" , "http://127.0.0.1:8787" )
475+ monkeypatch .setattr ("mcpgateway.transports.rust_mcp_runtime_proxy.get_http_client" , AsyncMock (return_value = client ))
476+
477+ fallback = AsyncMock ()
478+ proxy = RustMCPRuntimeProxy (fallback )
479+ events = []
480+
481+ async def send (message ):
482+ events .append (message )
483+
484+ try :
485+ await proxy .handle_streamable_http (
486+ {
487+ "type" : "http" ,
488+ "method" : "GET" ,
489+ "path" : "/" ,
490+ "modified_path" : "/mcp" ,
491+ "query_string" : b"session_id=abc123" ,
492+ "headers" : [],
493+ },
494+ _make_receive (b"" ),
495+ send ,
496+ )
497+ finally :
498+ await client .aclose ()
499+
500+ fallback .assert_not_awaited ()
501+ assert requests_seen == ["http://127.0.0.1:8787/mcp/?session_id=abc123" ]
502+ assert events [0 ]["status" ] == 307
503+ assert (b"location" , b"http://127.0.0.1:8787/final" ) in events [0 ]["headers" ]
444504 assert events [- 1 ] == {"type" : "http.response.body" , "body" : b"" , "more_body" : False }
445505
446506
0 commit comments