From 3ebd3e2cf4d417f3cdd6d34ec4a4b24f0f11f225 Mon Sep 17 00:00:00 2001 From: Thomas Sarlandie Date: Tue, 28 Jul 2026 16:53:09 +0000 Subject: [PATCH 1/2] fix: preserve transient OAuth discovery HTTP errors --- crates/rmcp/src/transport/auth.rs | 85 +++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index ef562ffda..3694a8b14 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2635,10 +2635,14 @@ impl AuthorizationManager { request: OAuthHttpRequest, ) -> Result { let response = self.http_client.execute(request).await?; - if response.status().is_server_error() { - return Err(Box::new(OAuthHttpError::UnexpectedStatus( - response.status(), - ))); + let status = response.status(); + if status.is_server_error() + || matches!( + status, + StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_MANY_REQUESTS + ) + { + return Err(Box::new(OAuthHttpError::UnexpectedStatus(status))); } Ok(response) } @@ -3820,6 +3824,7 @@ mod tests { }; use oauth2::{AuthType, CsrfToken, HttpResponse, PkceCodeVerifier}; + use reqwest::StatusCode; use rstest::rstest; use url::Url; @@ -3987,6 +3992,63 @@ mod tests { ); } + #[rstest] + #[case::resource_request_timeout(StatusCode::REQUEST_TIMEOUT, 0, "https://mcp.example.com/mcp")] + #[case::resource_too_many_requests( + StatusCode::TOO_MANY_REQUESTS, + 0, + "https://mcp.example.com/mcp" + )] + #[case::protected_metadata_request_timeout( + StatusCode::REQUEST_TIMEOUT, + 1, + "https://mcp.example.com/.well-known/oauth-protected-resource" + )] + #[case::protected_metadata_too_many_requests( + StatusCode::TOO_MANY_REQUESTS, + 1, + "https://mcp.example.com/.well-known/oauth-protected-resource" + )] + #[case::authorization_request_timeout( + StatusCode::REQUEST_TIMEOUT, + 2, + "https://auth.example.com/.well-known/oauth-authorization-server" + )] + #[case::authorization_too_many_requests( + StatusCode::TOO_MANY_REQUESTS, + 2, + "https://auth.example.com/.well-known/oauth-authorization-server" + )] + #[tokio::test] + async fn discovery_propagates_transient_client_errors( + #[case] status: StatusCode, + #[case] successful_response_count: usize, + #[case] expected_url: &str, + ) { + let mut responses = preregistered_discovery_responses(); + responses.insert(successful_response_count, empty_response(status.as_u16())); + + let client = RecordingOAuthHttpClient::with_responses(responses); + let manager = AuthorizationManager::new_with_oauth_http_client( + "https://mcp.example.com/mcp", + Arc::new(client.clone()), + ) + .await + .unwrap(); + + let error = manager.resolve_metadata().await.unwrap_err(); + + assert!( + matches!( + error, + AuthError::MetadataError(ref reason) + if reason.contains(expected_url) && reason.contains(status.as_str()) + ), + "unexpected discovery error for {status}: {error}" + ); + assert_eq!(client.requests().len(), successful_response_count + 1); + } + #[tokio::test] async fn custom_http_client_handles_protected_resource_discovery() { let challenge = oauth2::http::Response::builder() @@ -4330,13 +4392,18 @@ mod tests { ); } + #[rstest] + #[case::not_found(StatusCode::NOT_FOUND)] + #[case::method_not_allowed(StatusCode::METHOD_NOT_ALLOWED)] #[tokio::test] - async fn resolve_metadata_reports_legacy_fallback_when_nothing_is_discovered() { + async fn resolve_metadata_reports_legacy_fallback_when_nothing_is_discovered( + #[case] status: StatusCode, + ) { let client = RecordingOAuthHttpClient::with_responses(vec![ - empty_response(404), - empty_response(404), - empty_response(404), - empty_response(404), + empty_response(status.as_u16()), + empty_response(status.as_u16()), + empty_response(status.as_u16()), + empty_response(status.as_u16()), ]); let manager = AuthorizationManager::new_with_oauth_http_client( "https://legacy.example.com/", From bf9e7c5ae22179559a16dc5d2c0730c597bee157 Mon Sep 17 00:00:00 2001 From: Thomas Sarlandie Date: Tue, 28 Jul 2026 18:16:42 +0000 Subject: [PATCH 2/2] fix: preserve HTTP 425 during OAuth discovery --- crates/rmcp/src/transport/auth.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index 3694a8b14..be8733384 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2639,7 +2639,7 @@ impl AuthorizationManager { if status.is_server_error() || matches!( status, - StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_MANY_REQUESTS + StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_EARLY | StatusCode::TOO_MANY_REQUESTS ) { return Err(Box::new(OAuthHttpError::UnexpectedStatus(status))); @@ -3994,6 +3994,7 @@ mod tests { #[rstest] #[case::resource_request_timeout(StatusCode::REQUEST_TIMEOUT, 0, "https://mcp.example.com/mcp")] + #[case::resource_too_early(StatusCode::TOO_EARLY, 0, "https://mcp.example.com/mcp")] #[case::resource_too_many_requests( StatusCode::TOO_MANY_REQUESTS, 0, @@ -4004,6 +4005,11 @@ mod tests { 1, "https://mcp.example.com/.well-known/oauth-protected-resource" )] + #[case::protected_metadata_too_early( + StatusCode::TOO_EARLY, + 1, + "https://mcp.example.com/.well-known/oauth-protected-resource" + )] #[case::protected_metadata_too_many_requests( StatusCode::TOO_MANY_REQUESTS, 1, @@ -4014,6 +4020,11 @@ mod tests { 2, "https://auth.example.com/.well-known/oauth-authorization-server" )] + #[case::authorization_too_early( + StatusCode::TOO_EARLY, + 2, + "https://auth.example.com/.well-known/oauth-authorization-server" + )] #[case::authorization_too_many_requests( StatusCode::TOO_MANY_REQUESTS, 2,