@@ -6,7 +6,7 @@ use reqwest::header::ACCEPT;
66use sse_stream:: { Sse , SseStream } ;
77
88use crate :: {
9- model:: { ClientJsonRpcMessage , ServerJsonRpcMessage } ,
9+ model:: { ClientJsonRpcMessage , JsonRpcMessage , ServerJsonRpcMessage } ,
1010 transport:: {
1111 common:: http_header:: {
1212 EVENT_STREAM_MIME_TYPE , HEADER_LAST_EVENT_ID , HEADER_MCP_PROTOCOL_VERSION ,
@@ -59,6 +59,15 @@ fn apply_custom_headers(
5959 Ok ( builder)
6060}
6161
62+ /// Attempts to parse `body` as a JSON-RPC error message.
63+ /// Returns `None` if the body is not parseable or is not a `JsonRpcMessage::Error`.
64+ fn parse_json_rpc_error ( body : & str ) -> Option < ServerJsonRpcMessage > {
65+ match serde_json:: from_str :: < ServerJsonRpcMessage > ( body) {
66+ Ok ( message @ JsonRpcMessage :: Error ( _) ) => Some ( message) ,
67+ _ => None ,
68+ }
69+ }
70+
6271impl StreamableHttpClient for reqwest:: Client {
6372 type Error = reqwest:: Error ;
6473
@@ -199,10 +208,8 @@ impl StreamableHttpClient for reqwest::Client {
199208 . get ( HEADER_SESSION_ID )
200209 . and_then ( |v| v. to_str ( ) . ok ( ) )
201210 . map ( |s| s. to_string ( ) ) ;
202- // For non-success responses, attempt to parse JSON-RPC error bodies
203- // before falling back to a transport error. HTTP 4xx responses with
204- // Content-Type: application/json may carry valid JSON-RPC error
205- // payloads that should be surfaced as McpError, not TransportSend.
211+ // Non-success responses may carry valid JSON-RPC error payloads that
212+ // should be surfaced as McpError rather than lost in TransportSend.
206213 if !status. is_success ( ) {
207214 let body = response
208215 . text ( )
@@ -212,12 +219,12 @@ impl StreamableHttpClient for reqwest::Client {
212219 . as_deref ( )
213220 . is_some_and ( |ct| ct. as_bytes ( ) . starts_with ( JSON_MIME_TYPE . as_bytes ( ) ) )
214221 {
215- match serde_json :: from_str :: < ServerJsonRpcMessage > ( & body) {
216- Ok ( message) => {
222+ match parse_json_rpc_error ( & body) {
223+ Some ( message) => {
217224 return Ok ( StreamableHttpPostResponse :: Json ( message, session_id) ) ;
218225 }
219- Err ( e ) => tracing:: warn!(
220- "HTTP {status}: could not parse JSON response as ServerJsonRpcMessage: {e} "
226+ None => tracing:: warn!(
227+ "HTTP {status}: could not parse JSON body as a JSON-RPC error "
221228 ) ,
222229 }
223230 }
@@ -327,8 +334,8 @@ fn extract_scope_from_header(header: &str) -> Option<String> {
327334
328335#[ cfg( test) ]
329336mod tests {
330- use super :: extract_scope_from_header;
331- use crate :: transport:: streamable_http_client:: InsufficientScopeError ;
337+ use super :: { extract_scope_from_header, parse_json_rpc_error } ;
338+ use crate :: { model :: JsonRpcMessage , transport:: streamable_http_client:: InsufficientScopeError } ;
332339
333340 #[ test]
334341 fn extract_scope_quoted ( ) {
@@ -375,4 +382,36 @@ mod tests {
375382 assert ! ( !without_scope. can_upgrade( ) ) ;
376383 assert_eq ! ( without_scope. get_required_scope( ) , None ) ;
377384 }
385+
386+ #[ test]
387+ fn parse_json_rpc_error_returns_error_variant ( ) {
388+ let body =
389+ r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}}"# ;
390+ assert ! ( matches!(
391+ parse_json_rpc_error( body) ,
392+ Some ( JsonRpcMessage :: Error ( _) )
393+ ) ) ;
394+ }
395+
396+ #[ test]
397+ fn parse_json_rpc_error_rejects_non_error_request ( ) {
398+ // A valid JSON-RPC request (method + id) must not be accepted as an error.
399+ let body = r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"# ;
400+ assert ! ( parse_json_rpc_error( body) . is_none( ) ) ;
401+ }
402+
403+ #[ test]
404+ fn parse_json_rpc_error_rejects_notification ( ) {
405+ // A notification (method, no id) must not be accepted as an error.
406+ let body =
407+ r#"{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":1}}"# ;
408+ assert ! ( parse_json_rpc_error( body) . is_none( ) ) ;
409+ }
410+
411+ #[ test]
412+ fn parse_json_rpc_error_rejects_malformed_json ( ) {
413+ assert ! ( parse_json_rpc_error( "not json at all" ) . is_none( ) ) ;
414+ assert ! ( parse_json_rpc_error( "" ) . is_none( ) ) ;
415+ assert ! ( parse_json_rpc_error( r#"{"broken":"# ) . is_none( ) ) ;
416+ }
378417}
0 commit comments