-
Notifications
You must be signed in to change notification settings - Fork 3.7k
test: add tests for non-2xx HTTP status handling (fixes #3091) #3093
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
zsxh1990
wants to merge
7
commits into
modelcontextprotocol:main
from
zsxh1990:test/non-2xx-status-handling
+133
−0
Closed
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d97ada4
test: add tests for non-2xx HTTP status handling (fixes #3091)
zsxh1990 b6f7fb2
fix: correct import path for SessionMessage
zsxh1990 233f368
fix: use proper StreamableHTTPTransport initialization in tests
zsxh1990 887ed85
fix: ruff lint fixes for test file
zsxh1990 2d0befb
fix: use MagicMock for async context manager in tests
zsxh1990 ce68b25
fix: use MagicMock with async __aenter__/__aexit__ for context manager
zsxh1990 8f5df96
fix: use MagicMock for client to support async context manager
zsxh1990 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| """Tests for non-2xx HTTP status handling in StreamableHTTPTransport. | ||
|
|
||
| Verifies that when the server returns 401/403/5xx, the caller receives | ||
| a proper JSONRPCError (not a timeout). | ||
|
|
||
| Closes #3091 | ||
| """ | ||
| import json | ||
| import pytest | ||
| import httpx | ||
| from unittest.mock import AsyncMock, MagicMock, patch | ||
|
|
||
| from mcp.client.streamable_http import StreamableHTTPTransport, RequestContext | ||
| from mcp_types import JSONRPCRequest, JSONRPCError, ErrorData, SessionMessage | ||
|
|
||
|
|
||
| class TestNon2xxStatusHandling: | ||
| """Test that non-2xx status codes produce proper error responses.""" | ||
|
|
||
| def _make_request_context(self, request_id: str = "test-123") -> MagicMock: | ||
| """Create a mock RequestContext.""" | ||
| ctx = MagicMock(spec=RequestContext) | ||
| ctx.session_message = MagicMock() | ||
| ctx.session_message.message = MagicMock(spec=JSONRPCRequest) | ||
| ctx.session_message.message.id = request_id | ||
| ctx.read_stream_writer = AsyncMock() | ||
| ctx.client = AsyncMock() | ||
| ctx.metadata = None | ||
| return ctx | ||
|
|
||
| @pytest.mark.anyio | ||
| async def test_401_produces_error_response(self): | ||
| """401 Unauthorized should produce a JSONRPCError with the request's ID.""" | ||
| # This test verifies the fix for #3091 | ||
| # When server returns 401, caller should get a proper error, not a timeout | ||
| transport = StreamableHTTPTransport.__new__(StreamableHTTPTransport) | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| transport.url = "http://test" | ||
| transport.session_id = None | ||
|
|
||
| ctx = self._make_request_context() | ||
|
|
||
| # Mock the response to return 401 | ||
| mock_response = MagicMock() | ||
| mock_response.status_code = 401 | ||
| mock_response.headers = {"content-type": "text/plain"} | ||
| mock_response.aread = AsyncMock(return_value=b"Unauthorized") | ||
|
|
||
| # Mock the context manager for stream() | ||
| mock_stream_ctx = AsyncMock() | ||
| mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response) | ||
| mock_stream_ctx.__aexit__ = AsyncMock(return_value=False) | ||
| ctx.client.stream.return_value = mock_stream_ctx | ||
|
|
||
| # Call _handle_post_request | ||
| await transport._handle_post_request(ctx) | ||
|
|
||
| # Verify that an error was sent to the read_stream_writer | ||
| ctx.read_stream_writer.send.assert_called_once() | ||
| sent_message = ctx.read_stream_writer.send.call_args[0][0] | ||
| assert isinstance(sent_message, SessionMessage) | ||
| assert isinstance(sent_message.message, JSONRPCError) | ||
| assert sent_message.message.id == "test-123" | ||
| assert sent_message.message.error.code == -32603 # INTERNAL_ERROR | ||
|
|
||
| @pytest.mark.anyio | ||
| async def test_403_produces_error_response(self): | ||
| """403 Forbidden should produce a JSONRPCError with the request's ID.""" | ||
| transport = StreamableHTTPTransport.__new__(StreamableHTTPTransport) | ||
| transport.url = "http://test" | ||
| transport.session_id = None | ||
|
|
||
| ctx = self._make_request_context() | ||
|
|
||
| mock_response = MagicMock() | ||
| mock_response.status_code = 403 | ||
| mock_response.headers = {"content-type": "text/plain"} | ||
| mock_response.aread = AsyncMock(return_value=b"Forbidden") | ||
|
|
||
| mock_stream_ctx = AsyncMock() | ||
| mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response) | ||
| mock_stream_ctx.__aexit__ = AsyncMock(return_value=False) | ||
| ctx.client.stream.return_value = mock_stream_ctx | ||
|
|
||
| await transport._handle_post_request(ctx) | ||
|
|
||
| ctx.read_stream_writer.send.assert_called_once() | ||
| sent_message = ctx.read_stream_writer.send.call_args[0][0] | ||
| assert isinstance(sent_message.message, JSONRPCError) | ||
| assert sent_message.message.id == "test-123" | ||
|
|
||
| @pytest.mark.anyio | ||
| async def test_500_produces_error_response(self): | ||
| """500 Internal Server Error should produce a JSONRPCError.""" | ||
| transport = StreamableHTTPTransport.__new__(StreamableHTTPTransport) | ||
| transport.url = "http://test" | ||
| transport.session_id = None | ||
|
|
||
| ctx = self._make_request_context() | ||
|
|
||
| mock_response = MagicMock() | ||
| mock_response.status_code = 500 | ||
| mock_response.headers = {"content-type": "text/plain"} | ||
| mock_response.aread = AsyncMock(return_value=b"Internal Server Error") | ||
|
|
||
| mock_stream_ctx = AsyncMock() | ||
| mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response) | ||
| mock_stream_ctx.__aexit__ = AsyncMock(return_value=False) | ||
| ctx.client.stream.return_value = mock_stream_ctx | ||
|
|
||
| await transport._handle_post_request(ctx) | ||
|
|
||
| ctx.read_stream_writer.send.assert_called_once() | ||
| sent_message = ctx.read_stream_writer.send.call_args[0][0] | ||
| assert isinstance(sent_message.message, JSONRPCError) | ||
|
|
||
| @pytest.mark.anyio | ||
| async def test_json_error_body_is_parsed(self): | ||
| """When server returns JSON-RPC error body, it should be used directly.""" | ||
| transport = StreamableHTTPTransport.__new__(StreamableHTTPTransport) | ||
| transport.url = "http://test" | ||
| transport.session_id = None | ||
|
|
||
| ctx = self._make_request_context() | ||
|
|
||
| error_body = json.dumps({ | ||
| "jsonrpc": "2.0", | ||
| "id": "test-123", | ||
| "error": {"code": -32600, "message": "Invalid Request"} | ||
| }).encode() | ||
|
|
||
| mock_response = MagicMock() | ||
| mock_response.status_code = 400 | ||
| mock_response.headers = {"content-type": "application/json"} | ||
| mock_response.aread = AsyncMock(return_value=error_body) | ||
|
|
||
| mock_stream_ctx = AsyncMock() | ||
| mock_stream_ctx.__aenter__ = AsyncMock(return_value=mock_response) | ||
| mock_stream_ctx.__aexit__ = AsyncMock(return_value=False) | ||
| ctx.client.stream.return_value = mock_stream_ctx | ||
|
|
||
| await transport._handle_post_request(ctx) | ||
|
|
||
| ctx.read_stream_writer.send.assert_called_once() | ||
| sent_message = ctx.read_stream_writer.send.call_args[0][0] | ||
| assert isinstance(sent_message.message, JSONRPCError) | ||
| assert sent_message.message.error.code == -32600 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.