Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Don't treat non-success HTTP codes as transport errors by SteffenDE · Pull Request #486 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Don't treat non-success HTTP codes as transport errors by SteffenDE · Pull Request #486 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Don't treat non-success HTTP codes as transport errors by SteffenDE · Pull Request #486 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Don't treat non-success HTTP codes as transport errors by SteffenDE · Pull Request #486 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Don't treat non-success HTTP codes as transport errors by SteffenDE · Pull Request #486 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Don't treat non-success HTTP codes as transport errors by SteffenDE · Pull Request #486 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Don't treat non-success HTTP codes as transport errors by SteffenDE · Pull Request #486 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Don't treat non-success HTTP codes as transport errors - #486

Closed
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request
Closed

Don't treat non-success HTTP codes as transport errors#486
SteffenDE wants to merge 1 commit into
modelcontextprotocol:mainfrom
SteffenDE:sd-bad-request

Conversation

@SteffenDE

Copy link
Copy Markdown

The spec states that:

If the server cannot accept the input, it MUST return an HTTP error
status code (e.g., 400 Bad Request).
The HTTP response body MAY comprise a JSON-RPC error response that has no id.

But the rust-sdk treated any POST responses with non successful status as an error.

Before this patch, trying to call list_resources on a server that does return HTTP 400 with JSON-RPC error:

TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)

After this patch:

McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)

Motivation and Context

Clients should be able to see the error the server sends. The code already checks for JSON content type, I assume the error_for_status for included by accident.

How Has This Been Tested?

Tests still seem to pass, but I did not add a new one. For testing, I created a demo client, but someone with more knowledge of the code should add a proper test case.

use anyhow::Result;use rmcp::{ServiceExt,
model::{ClientCapabilities,ClientInfo,Implementation},
transport::StreamableHttpClientTransport,};use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};#[tokio::main]asyncfnmain() -> Result<()>{
tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("debug,{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let transport = StreamableHttpClientTransport::from_uri("http://localhost:4000/mcp");let client_info = ClientInfo{protocol_version:Default::default(),capabilities:ClientCapabilities::default(),client_info:Implementation{name:"test unknown method client".to_string(),title:None,version:"0.0.1".to_string(),website_url:None,icons:None,},};let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error during connection: {:?}", e);})?;let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
tracing::info!("Calling list_resources (which the server doesn't support)...");match client.list_resources(Default::default()).await{Ok(result) => {
tracing::info!("Success: {result:#?}");}Err(e) => {
tracing::error!("Error calling list_resources: {e:#?}");}}
client.cancel().await?;Ok(())}

Breaking Changes

Could be seen as a breaking change if people relied on getting the transport error.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The spec states that:
> If the server cannot accept the input, it MUST return an HTTP error
> status code (e.g., 400 Bad Request).
> The HTTP response body MAY comprise a JSON-RPC error response that has no id.
But the rust-sdk treated any POST responses with non successful status
as an error.
Before this patch, trying to call list_resources on a server that does
return HTTP 400 with JSON-RPC error:
```
TransportSend(
DynamicTransportError {
transport_name: "rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<reqwest::async_impl::client::Client>>",
transport_type_id: TypeId(0xf7bacf3cf3889d471ead32d7a3cc8155),
error: Client(
reqwest::Error {
kind: Status(
400,
None,
),
url: "http://localhost:4000/mcp",
},
),
},
)
```
After this patch:
```
McpError(
ErrorData {
code: ErrorCode(
-32601,
),
message: "Method not found",
data: Some(
Object {
"name": String("resources/list"),
},
),
},
)
```

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes HTTP transport error handling to properly distinguish between transport-level failures and application-level JSON-RPC errors returned via HTTP error status codes, aligning with the MCP specification.

  • Removes error_for_status() calls that incorrectly treated HTTP error codes as transport errors
  • Allows JSON-RPC error responses with HTTP 4xx/5xx status codes to be parsed and returned as proper MCP errors
  • Enables clients to receive server-sent error details instead of generic HTTP transport errors

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rsRemoves error_for_status() check to allow non-2xx responses to be processed as potential JSON-RPC errors
crates/rmcp/src/transport/common/reqwest/sse_client.rsRemoves error_for_status() check for SSE transport consistency

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@4t145

Copy link
Copy Markdown
Contributor

Good catch, but should we have some log when response status is not success?

@SteffenDE

Copy link
Copy Markdown
Author

I don’t think there’s a need to log, since it’s part of the spec to have non-200 codes and a client will still receive this as an error, so users can decide to log if they want.

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It LGTM. Sorry for the delay in getting to it.

@SteffenDE Do you mind rebasing and having the commit message conform to https://www.conventionalcommits.org, then we can merge.

@SteffenDE

Copy link
Copy Markdown
Author

@alexhancock No worries! We're still using the SSE code, so I don't want to update that branch for now. As it's only one line to remove

let response = response.error_for_status()?;

I think it's fine if I just close the PR and you do that change directly :)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-coreCore library changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@SteffenDE@4t145@alexhancock@scutuatua-crypto