Apply the request body limit to the SSE and OAuth endpoints (#3336)
This commit is contained in:
parent
0cee6249ba
commit
57394b0548
1 changed files with 420 additions and 189 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue