Skip to content

Commit c09c6a3

Browse files
committed
fix(rmcp): surface JSON-RPC error bodies on HTTP 4xx responses
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
1 parent 7e03278 commit c09c6a3

1 file changed

Lines changed: 30 additions & 28 deletions

File tree

crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,10 @@ impl StreamableHttpClient for reqwest::Client {
190190
if status == reqwest::StatusCode::NOT_FOUND && session_was_attached {
191191
return Err(StreamableHttpError::SessionExpired);
192192
}
193-
let content_type = response.headers().get(reqwest::header::CONTENT_TYPE);
194-
let is_json = content_type
195-
.map(|ct| ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes()))
196-
.unwrap_or(false);
197-
let is_sse = content_type
198-
.map(|ct| ct.as_bytes().starts_with(EVENT_STREAM_MIME_TYPE.as_bytes()))
199-
.unwrap_or(false);
200-
let content_type =
201-
content_type.map(|ct| String::from_utf8_lossy(ct.as_bytes()).to_string());
193+
let content_type = response
194+
.headers()
195+
.get(reqwest::header::CONTENT_TYPE)
196+
.map(|ct| String::from_utf8_lossy(ct.as_bytes()).to_string());
202197
let session_id = response
203198
.headers()
204199
.get(HEADER_SESSION_ID)
@@ -213,7 +208,10 @@ impl StreamableHttpClient for reqwest::Client {
213208
.text()
214209
.await
215210
.unwrap_or_else(|_| "<failed to read response body>".to_owned());
216-
if is_json {
211+
if content_type
212+
.as_deref()
213+
.is_some_and(|ct| ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes()))
214+
{
217215
match serde_json::from_str::<ServerJsonRpcMessage>(&body) {
218216
Ok(message) => {
219217
return Ok(StreamableHttpPostResponse::Json(message, session_id));
@@ -227,26 +225,30 @@ impl StreamableHttpClient for reqwest::Client {
227225
format!("HTTP {status}: {body}"),
228226
)));
229227
}
230-
if is_sse {
231-
let event_stream = SseStream::from_byte_stream(response.bytes_stream()).boxed();
232-
Ok(StreamableHttpPostResponse::Sse(event_stream, session_id))
233-
} else if is_json {
234-
// Try to parse as a valid JSON-RPC message. If the body is
235-
// malformed (e.g. a 200 response to a notification that lacks
236-
// an `id` field), treat it as accepted rather than failing.
237-
match response.json::<ServerJsonRpcMessage>().await {
238-
Ok(message) => Ok(StreamableHttpPostResponse::Json(message, session_id)),
239-
Err(e) => {
240-
tracing::warn!(
241-
"could not parse JSON response as ServerJsonRpcMessage, treating as accepted: {e}"
242-
);
243-
Ok(StreamableHttpPostResponse::Accepted)
228+
match content_type.as_deref() {
229+
Some(ct) if ct.as_bytes().starts_with(EVENT_STREAM_MIME_TYPE.as_bytes()) => {
230+
let event_stream = SseStream::from_byte_stream(response.bytes_stream()).boxed();
231+
Ok(StreamableHttpPostResponse::Sse(event_stream, session_id))
232+
}
233+
Some(ct) if ct.as_bytes().starts_with(JSON_MIME_TYPE.as_bytes()) => {
234+
// Try to parse as a valid JSON-RPC message. If the body is
235+
// malformed (e.g. a 200 response to a notification that lacks
236+
// an `id` field), treat it as accepted rather than failing.
237+
match response.json::<ServerJsonRpcMessage>().await {
238+
Ok(message) => Ok(StreamableHttpPostResponse::Json(message, session_id)),
239+
Err(e) => {
240+
tracing::warn!(
241+
"could not parse JSON response as ServerJsonRpcMessage, treating as accepted: {e}"
242+
);
243+
Ok(StreamableHttpPostResponse::Accepted)
244+
}
244245
}
245246
}
246-
} else {
247-
// unexpected content type
248-
tracing::error!("unexpected content type: {:?}", content_type);
249-
Err(StreamableHttpError::UnexpectedContentType(content_type))
247+
_ => {
248+
// unexpected content type
249+
tracing::error!("unexpected content type: {:?}", content_type);
250+
Err(StreamableHttpError::UnexpectedContentType(content_type))
251+
}
250252
}
251253
}
252254
}

0 commit comments

Comments
 (0)