Apply the request body limit to the SSE and OAuth endpoints (#3336)

This commit is contained in:
Max 2026-08-20 16:21:44 +01:00 • committed by GitHub
commit 57394b0548
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,9 +1,17 @@
"""Tests for the transport-security request validation middleware."""
"""Tests for the request checks shared by the HTTP server transports."""
from collections.abc import Iterator
from unittest.mock import AsyncMock
import pytest
from starlette.requests import Request
from starlette.types import Message, Receive, Scope, Send
from mcp.server.transport_security import TransportSecurityMiddleware, TransportSecuritySettings
from mcp.server.transport_security import (
RequestBodyLimitMiddleware,
TransportSecurityMiddleware,
TransportSecuritySettings,
)
def _request(host: str | None, origin: str | None, content_type: str | None = "application/json") -> Request:
@ -86,3 +94,111 @@ async def test_validate_request_ignores_content_type_on_get() -> None:
middleware = TransportSecurityMiddleware(SETTINGS)
response = await middleware.validate_request(_request("good.example", None, content_type="text/plain"))
assert response is None
@pytest.mark.anyio
async def test_client_disconnect_while_streaming_request_body_is_replayed() -> None:
"""SDK-defined: raw ASGI is required to prove a disconnect before body completion reaches the transport."""
disconnect: Message = {"type": "http.disconnect"}
request_messages: Iterator[Message] = iter(
[{"type": "http.request", "body": b"1234", "more_body": True}, disconnect]
)
received_messages: list[Message] = []
async def receive() -> Message:
return next(request_messages)
async def app(scope: Scope, receive: Receive, send: Send) -> None:
received_messages.append(await receive())
received_messages.append(await receive())
scope: Scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []}
middleware = RequestBodyLimitMiddleware(app, max_body_size=8)
await middleware(scope, receive, AsyncMock())
assert received_messages == [
{"type": "http.request", "body": b"1234", "more_body": True},
disconnect,
]
@pytest.mark.anyio
async def test_client_disconnect_before_request_body_is_replayed() -> None:
"""SDK-defined: raw ASGI proves a disconnect before the first body message reaches the transport."""
disconnect: Message = {"type": "http.disconnect"}
received_messages: list[Message] = []
async def receive() -> Message:
return disconnect
async def app(scope: Scope, receive: Receive, send: Send) -> None:
received_messages.append(await receive())
scope: Scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []}
middleware = RequestBodyLimitMiddleware(app, max_body_size=8)
await middleware(scope, receive, AsyncMock())
assert received_messages == [disconnect]
@pytest.mark.anyio
async def test_request_body_chunks_are_replayed_as_one_message() -> None:
"""SDK-defined: raw ASGI proves chunk overhead is discarded before the body reaches the transport."""
request_messages: Iterator[Message] = iter(
[
{"type": "http.request", "body": b"12", "more_body": True},
{"type": "http.request", "body": b"34", "more_body": True},
{"type": "http.request", "body": b"56", "more_body": False},
]
)
received_messages: list[Message] = []
async def receive() -> Message:
return next(request_messages)
async def app(scope: Scope, receive: Receive, send: Send) -> None:
received_messages.append(await receive())
scope: Scope = {"type": "http", "method": "POST", "path": "/mcp", "headers": []}
middleware = RequestBodyLimitMiddleware(app, max_body_size=8)
await middleware(scope, receive, AsyncMock())
assert received_messages == [{"type": "http.request", "body": b"123456", "more_body": False}]
@pytest.mark.anyio
@pytest.mark.parametrize("method", ["GET", "PUT", "OPTIONS", "HEAD", "DELETE"])
async def test_request_body_limit_applies_to_every_method(method: str) -> None:
"""SDK-defined: the limit is a property of the request body, not of the method that carries it."""
app = AsyncMock()
sent_messages: list[Message] = []
receive = AsyncMock(return_value={"type": "http.request", "body": b"123456789", "more_body": False})
async def send(message: Message) -> None:
sent_messages.append(message)
scope: Scope = {"type": "http", "method": method, "path": "/mcp", "headers": []}
middleware = RequestBodyLimitMiddleware(app, max_body_size=8)
await middleware(scope, receive, send)
assert [message["status"] for message in sent_messages if message["type"] == "http.response.start"] == [413]
app.assert_not_awaited()
@pytest.mark.anyio
async def test_request_body_limit_leaves_non_http_scopes_alone() -> None:
"""SDK-defined: only HTTP requests carry a body to limit; other ASGI scopes go straight to the app."""
app = AsyncMock()
receive = AsyncMock()
send = AsyncMock()
scope: Scope = {"type": "lifespan"}
middleware = RequestBodyLimitMiddleware(app, max_body_size=8)
await middleware(scope, receive, send)
app.assert_awaited_once_with(scope, receive, send)
receive.assert_not_awaited()