Skip to content

Commit 54b0cc4

Browse files
committed
Address request body limit review feedback
1 parent 37e41a2 commit 54b0cc4

2 files changed

Lines changed: 82 additions & 14 deletions

File tree

src/mcp/server/streamable_http_manager.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def __init__(
100100
self.retry_interval = retry_interval
101101
self.session_idle_timeout = session_idle_timeout
102102
self.max_request_body_size = max_request_body_size
103-
self.asgi_app = RequestBodyLimitMiddleware(self.handle_request, max_request_body_size)
103+
self.asgi_app = RequestBodyLimitMiddleware(self._handle_request, max_request_body_size)
104104

105105
# Session tracking (only used if not stateless)
106106
self._session_creation_lock = anyio.Lock()
@@ -167,6 +167,9 @@ async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> No
167167
168168
Dispatches to the appropriate handler based on stateless mode.
169169
"""
170+
await self.asgi_app(scope, receive, send)
171+
172+
async def _handle_request(self, scope: Scope, receive: Receive, send: Send) -> None:
170173
if self._task_group is None:
171174
raise RuntimeError("Task group is not initialized. Make sure to use run().")
172175

@@ -392,22 +395,34 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
392395
response = Response("Request body too large", status_code=413)
393396
return await response(scope, receive, send)
394397

395-
cached_messages: deque[Message] = deque()
396-
received_size = 0
398+
received_body = bytearray()
399+
received_request = False
400+
body_complete = False
401+
trailing_message: Message | None = None
397402
while True:
398403
message = await receive()
399404
if message["type"] != "http.request":
400-
cached_messages.append(message)
405+
trailing_message = message
401406
break
402407

403-
received_size += len(message.get("body", b""))
404-
if received_size > self.max_body_size:
408+
received_request = True
409+
body = message.get("body", b"")
410+
if len(received_body) + len(body) > self.max_body_size:
405411
response = Response("Request body too large", status_code=413)
406412
return await response(scope, receive, send)
407-
cached_messages.append(message)
413+
received_body.extend(body)
408414
if not message.get("more_body", False):
415+
body_complete = True
409416
break
410417

418+
cached_messages: deque[Message] = deque()
419+
if received_request:
420+
cached_messages.append(
421+
{"type": "http.request", "body": bytes(received_body), "more_body": not body_complete}
422+
)
423+
if trailing_message is not None:
424+
cached_messages.append(trailing_message)
425+
411426
async def replay() -> Message:
412427
if cached_messages:
413428
return cached_messages.popleft()

tests/server/test_streamable_http_manager.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ async def test_handle_request_without_run_raises_error():
7676
manager = StreamableHTTPSessionManager(app=app)
7777

7878
# Mock ASGI parameters
79-
scope = {"type": "http", "method": "POST", "path": "/test"}
79+
scope: Scope = {"type": "http", "method": "POST", "path": "/test", "headers": []}
8080

81-
async def receive(): # pragma: no cover
81+
async def receive() -> Message:
8282
return {"type": "http.request", "body": b""}
8383

8484
async def send(message: Message): # pragma: no cover
@@ -108,7 +108,7 @@ async def send(message: Message) -> None:
108108
"headers": [(b"content-length", b"9")],
109109
}
110110
async with manager.run():
111-
await manager.asgi_app(scope, receive, send)
111+
await manager.handle_request(scope, receive, send)
112112
assert manager._server_instances == {}
113113

114114
response_start = next(message for message in sent_messages if message["type"] == "http.response.start")
@@ -150,6 +150,33 @@ async def send(message: Message) -> None:
150150
async def test_client_disconnect_while_streaming_request_body_is_replayed() -> None:
151151
"""SDK-defined: raw ASGI is required to prove a disconnect before body completion reaches the transport."""
152152
disconnect: Message = {"type": "http.disconnect"}
153+
request_messages: Iterator[Message] = iter(
154+
[{"type": "http.request", "body": b"1234", "more_body": True}, disconnect]
155+
)
156+
received_messages: list[Message] = []
157+
158+
async def receive() -> Message:
159+
return next(request_messages)
160+
161+
async def app(scope: Scope, receive: Receive, send: Send) -> None:
162+
received_messages.append(await receive())
163+
received_messages.append(await receive())
164+
165+
scope: Scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []}
166+
middleware = RequestBodyLimitMiddleware(app, max_body_size=8)
167+
168+
await middleware(scope, receive, AsyncMock())
169+
170+
assert received_messages == [
171+
{"type": "http.request", "body": b"1234", "more_body": True},
172+
disconnect,
173+
]
174+
175+
176+
@pytest.mark.anyio
177+
async def test_client_disconnect_before_request_body_is_replayed() -> None:
178+
"""SDK-defined: raw ASGI proves a disconnect before the first body message reaches the transport."""
179+
disconnect: Message = {"type": "http.disconnect"}
153180
received_messages: list[Message] = []
154181

155182
async def receive() -> Message:
@@ -166,6 +193,32 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None:
166193
assert received_messages == [disconnect]
167194

168195

196+
@pytest.mark.anyio
197+
async def test_request_body_chunks_are_replayed_as_one_message() -> None:
198+
"""SDK-defined: raw ASGI proves chunk overhead is discarded before the body reaches the transport."""
199+
request_messages: Iterator[Message] = iter(
200+
[
201+
{"type": "http.request", "body": b"12", "more_body": True},
202+
{"type": "http.request", "body": b"34", "more_body": True},
203+
{"type": "http.request", "body": b"56", "more_body": False},
204+
]
205+
)
206+
received_messages: list[Message] = []
207+
208+
async def receive() -> Message:
209+
return next(request_messages)
210+
211+
async def app(scope: Scope, receive: Receive, send: Send) -> None:
212+
received_messages.append(await receive())
213+
214+
scope: Scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []}
215+
middleware = RequestBodyLimitMiddleware(app, max_body_size=8)
216+
217+
await middleware(scope, receive, AsyncMock())
218+
219+
assert received_messages == [{"type": "http.request", "body": b"123456", "more_body": False}]
220+
221+
169222
def test_request_body_limit_defaults_to_four_mib() -> None:
170223
"""SDK-defined: Streamable HTTP request bodies are limited to 4 MiB by default."""
171224
manager = StreamableHTTPSessionManager(app=Server("test-default-size-limit"))
@@ -216,7 +269,7 @@ async def mock_send(message: Message):
216269
"headers": [(b"content-type", b"application/json")],
217270
}
218271

219-
async def mock_receive(): # pragma: no cover
272+
async def mock_receive():
220273
return {"type": "http.request", "body": b"", "more_body": False}
221274

222275
# Trigger session creation
@@ -274,7 +327,7 @@ async def mock_send(message: Message):
274327
"headers": [(b"content-type", b"application/json")],
275328
}
276329

277-
async def mock_receive(): # pragma: no cover
330+
async def mock_receive():
278331
return {"type": "http.request", "body": b"", "more_body": False}
279332

280333
# Trigger session creation
@@ -392,7 +445,7 @@ async def mock_send(message: Message):
392445
}
393446

394447
async def mock_receive():
395-
return {"type": "http.request", "body": b"{}", "more_body": False} # pragma: no cover
448+
return {"type": "http.request", "body": b"{}", "more_body": False}
396449

397450
with caplog.at_level(logging.INFO):
398451
await manager.handle_request(scope, mock_receive, mock_send)
@@ -472,7 +525,7 @@ async def mock_send(message: Message):
472525
"headers": [(b"content-type", b"application/json")],
473526
}
474527

475-
async def mock_receive(): # pragma: no cover
528+
async def mock_receive():
476529
return {"type": "http.request", "body": b"", "more_body": False}
477530

478531
await manager.handle_request(scope, mock_receive, mock_send)

0 commit comments

Comments
 (0)