fix(rmcp): surface JSON-RPC error bodies on HTTP 4xx responses#748
fix(rmcp): surface JSON-RPC error bodies on HTTP 4xx responses#748kakarot-dev wants to merge 3 commits intomodelcontextprotocol:mainfrom
Conversation
When a server returns a 4xx status with Content-Type: application/json, attempt to deserialize the body as a ServerJsonRpcMessage before falling back to UnexpectedServerResponse. This allows JSON-RPC error payloads carried on HTTP error responses to be surfaced as McpError instead of being lost in a transport-level error string. Fixes modelcontextprotocol#724
When a server returns a 4xx status with Content-Type: application/json, attempt to deserialize the body as a ServerJsonRpcMessage before falling back to UnexpectedServerResponse. This allows JSON-RPC error payloads carried on HTTP error responses to be surfaced as McpError instead of being lost in a transport-level error string. Fixes modelcontextprotocol#724
DaleSeo
left a comment
There was a problem hiding this comment.
Thanks for tackling this, @kakarot-dev! I have a couple of comments below. It looks like you've done some manual testing, but it would be great if you could add some automated tests to avoid any regressions.
| .as_deref() | ||
| .is_some_and(|ct| ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes())) | ||
| { | ||
| match serde_json::from_str::<ServerJsonRpcMessage>(&body) { |
There was a problem hiding this comment.
ServerJsonRpcMessage can parse any valid JSON-RPC message including Request, Response, Notification, not just Error. The deserialization should verify the parsed message is actually a JsonRpcMessage::Error.
There was a problem hiding this comment.
sorry about that, updated it so that it now only matches JsonRpcMessage::Error
| // For non-success responses, attempt to parse JSON-RPC error bodies | ||
| // before falling back to a transport error. HTTP 4xx responses with | ||
| // Content-Type: application/json may carry valid JSON-RPC error | ||
| // payloads that should be surfaced as McpError, not TransportSend. |
There was a problem hiding this comment.
The comment says "HTTP 4xx responses" but the guard is !status.is_success(), which also covers 1xx, 3xx, and 5xx. This could be clearer and simpler.
| // For non-success responses, attempt to parse JSON-RPC error bodies | |
| // before falling back to a transport error. HTTP 4xx responses with | |
| // Content-Type: application/json may carry valid JSON-RPC error | |
| // payloads that should be surfaced as McpError, not TransportSend. | |
| // Non-success responses may carry valid JSON-RPC error payloads that | |
| // should be surfaced as McpError rather than lost in TransportSend. |
There was a problem hiding this comment.
updated the comments, thank you
When a server returns a 4xx status with Content-Type: application/json, attempt to deserialize the body as a ServerJsonRpcMessage before falling back to UnexpectedServerResponse. This allows JSON-RPC error payloads carried on HTTP error responses to be surfaced as McpError instead of being lost in a transport-level error string.
Fixes #724
Attempt to deserialize HTTP 4xx response bodies as ServerJsonRpcMessage when Content-Type: application/json, surfacing JSON-RPC errors as McpError instead of discarding them as UnexpectedServerResponse.
How Has This Been Tested?
Tested locally with a mock axum server against three scenarios: valid JSON-RPC error body on 400, non-JSON 400, and malformed JSON on 400. All pass.
Breaking Changes
None.
Types of changes
Checklist
Additional context
I created a test file locally, Test file available if needed