refactor: make oauth discovery reactive (#1052)

This commit is contained in:
Dale Seo 2026-07-27 10:43:06 -04:00 • committed by GitHub
commit c284607794
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 581 additions and 241 deletions

View file

@ -86,6 +86,31 @@ impl ClientInitializeError {
context: context.into(),
}
}
/// The `WWW-Authenticate` challenge from the 401/403 the transport hit
/// during initialization, if that is why initialization failed.
///
/// This is the trigger of the reactive OAuth flow: feed the challenge to
/// `AuthorizationRequest::with_challenge` to authorize, then reconnect.
#[cfg(feature = "transport-streamable-http-client")]
pub fn auth_challenge(&self) -> Option<&str> {
use crate::transport::streamable_http_client::{AuthRequiredError, InsufficientScopeError};
let Self::TransportError { error, .. } = self else {
return None;
};
let mut source: Option<&(dyn std::error::Error + 'static)> = Some(error.error.as_ref());
while let Some(current) = source {
if let Some(auth_required) = current.downcast_ref::<AuthRequiredError>() {
return Some(&auth_required.www_authenticate_header);
}
if let Some(insufficient_scope) = current.downcast_ref::<InsufficientScopeError>() {
return Some(&insufficient_scope.www_authenticate_header);
}
source = current.source();
}
None
}
}
/// Helper function to get the next message from the stream

View file

@ -31,12 +31,6 @@ use crate::transport::common::http_header::HEADER_MCP_PROTOCOL_VERSION;
const DEFAULT_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_OAUTH_HTTP_RESPONSE_BODY_BYTES: usize = 1024 * 1024;
const MAX_OAUTH_DISCOVERY_REDIRECTS: usize = 10;
const RESOURCE_METADATA_POST_PROBE_BODY: &str = concat!(
r#"{"jsonrpc":"2.0","id":"auth-discovery","method":"initialize","params":{"#,
r#""protocolVersion":"2024-11-05","capabilities":{},"#,
r#""clientInfo":{"name":"rmcp-auth-discovery","version":"0.0.0"}}"#,
r#"}"#
);
const CLOUD_METADATA_HOSTS: &[&str] = &[
"metadata",
"metadata.google.internal",
@ -634,6 +628,12 @@ pub struct WWWAuthenticateParams {
}
impl WWWAuthenticateParams {
/// Parse a `WWW-Authenticate` header value, resolving a relative
/// `resource_metadata` URL against `base_url`.
pub fn parse(header: &str, base_url: &Url) -> Self {
AuthorizationManager::extract_www_authenticate_params(header, base_url)
}
/// check if this is an insufficient_scope error
pub fn is_insufficient_scope(&self) -> bool {
self.error.as_deref() == Some("insufficient_scope")
@ -736,6 +736,11 @@ pub struct AuthorizationRequest {
/// OIDC Dynamic Client Registration `application_type` (SEP-837),
/// e.g. `"native"` or `"web"`.
pub application_type: Option<String>,
/// `WWW-Authenticate` header value from a real request's 401 response.
/// When set, discovery is seeded from the challenge (its
/// `resource_metadata` URL and `scope` hint) instead of probing the
/// server — the reactive discovery path.
pub challenge: Option<String>,
}
impl AuthorizationRequest {
@ -750,6 +755,7 @@ impl AuthorizationRequest {
client_secret: None,
client_metadata_url: None,
application_type: None,
challenge: None,
}
}
@ -805,6 +811,13 @@ impl AuthorizationRequest {
self.application_type = Some(application_type.into());
self
}
/// Seed discovery from the `WWW-Authenticate` header value of a real
/// request's 401 response instead of probing the server.
pub fn with_challenge(mut self, www_authenticate: impl Into<String>) -> Self {
self.challenge = Some(www_authenticate.into());
self
}
}
// add type aliases for oauth2 types
@ -1419,6 +1432,52 @@ impl AuthorizationManager {
})
}
/// Resolve authorization server metadata starting from the
/// `WWW-Authenticate` challenge of a real request's 401 response — the
/// reactive discovery path, matching the TypeScript and Python SDKs.
///
/// Seeds scope selection with the challenge's `scope` hint and prefers
/// the challenge's `resource_metadata` URL; falls back to
/// [`resolve_metadata`](Self::resolve_metadata) when the challenge is
/// `None` or carries no usable metadata pointer.
pub async fn resolve_metadata_from_challenge(
&self,
www_authenticate: Option<&str>,
) -> Result<AuthorizationMetadataResolution, AuthError> {
let Some(www_authenticate) = www_authenticate else {
return self.resolve_metadata().await;
};
let params = WWWAuthenticateParams::parse(www_authenticate, &self.base_url);
self.record_challenge_scope(&params).await;
if let Some(resource_metadata_url) = &params.resource_metadata_url
&& let Some(metadata) = self
.discover_oauth_server_from_resource_metadata_url(resource_metadata_url)
.await?
{
return Ok(AuthorizationMetadataResolution {
metadata,
source: AuthorizationMetadataSource::ProtectedResourceMetadata,
});
}
self.resolve_metadata().await
}
/// Store a challenge's `scope` hint for later scope selection.
async fn record_challenge_scope(&self, params: &WWWAuthenticateParams) {
let Some(scope) = &params.scope else {
return;
};
let scopes: Vec<String> = scope.split_whitespace().map(str::to_string).collect();
if scopes.is_empty() {
return;
}
debug!("WWW-Authenticate challenge contains scope: {scope}");
*self.www_auth_scopes.write().await = scopes;
}
fn legacy_authorization_metadata(base_url: &Url) -> AuthorizationMetadata {
let endpoint = |path: &str| {
let mut url = base_url.clone();
@ -2056,7 +2115,7 @@ impl AuthorizationManager {
/// refresh token or the server rejected it, return `AuthorizationRequired`
/// so the caller can re-prompt the user. Infrastructure errors (e.g. store
/// I/O failures, misconfigured client) are propagated as-is.
async fn try_refresh_or_reauth(&self) -> Result<String, AuthError> {
pub(crate) async fn try_refresh_or_reauth(&self) -> Result<String, AuthError> {
match self.refresh_token().await {
Ok(new_creds) => {
tracing::info!("Refreshed access token.");
@ -2328,12 +2387,19 @@ impl AuthorizationManager {
async fn discover_oauth_server_via_resource_metadata(
&self,
) -> Result<Option<AuthorizationMetadata>, AuthError> {
let Some(resource_metadata_url) = self.discover_resource_metadata_url().await? else {
let Some(resource_metadata_url) = self.discover_resource_metadata_url().await else {
return Ok(None);
};
self.discover_oauth_server_from_resource_metadata_url(&resource_metadata_url)
.await
}
async fn discover_oauth_server_from_resource_metadata_url(
&self,
resource_metadata_url: &Url,
) -> Result<Option<AuthorizationMetadata>, AuthError> {
let Some(resource_metadata) = self
.fetch_resource_metadata_from_url(&resource_metadata_url)
.fetch_resource_metadata_from_url(resource_metadata_url)
.await?
else {
return Ok(None);
@ -2443,11 +2509,10 @@ impl AuthorizationManager {
&& Self::is_same_origin(&root_resource, &path_resource)
}
async fn discover_resource_metadata_url(&self) -> Result<Option<Url>, AuthError> {
if let Ok(Some(resource_metadata_url)) =
self.fetch_resource_metadata_url(&self.base_url, true).await
async fn discover_resource_metadata_url(&self) -> Option<Url> {
if let Some(resource_metadata_url) = self.probe_resource_metadata_url(&self.base_url).await
{
return Ok(Some(resource_metadata_url));
return Some(resource_metadata_url);
}
// If the primary URL doesn't use WWW-Authenticate, try oauth-protected-resource discovery.
@ -2459,86 +2524,42 @@ impl AuthorizationManager {
discovery_url.set_query(None);
discovery_url.set_fragment(None);
discovery_url.set_path(&candidate_path);
if let Ok(Some(resource_metadata_url)) = self
.fetch_resource_metadata_url(&discovery_url, false)
.await
if let Some(resource_metadata_url) =
self.probe_resource_metadata_url(&discovery_url).await
{
return Ok(Some(resource_metadata_url));
return Some(resource_metadata_url);
}
}
Ok(None)
None
}
/// Extract the resource metadata url from the WWW-Authenticate header value.
/// Probe `url` with a GET, extracting the resource metadata url from a
/// 200 (the url itself is the metadata document) or from a 401's
/// WWW-Authenticate header value.
/// https://www.rfc-editor.org/rfc/rfc9728.html#name-use-of-www-authenticate-for
async fn fetch_resource_metadata_url(
&self,
url: &Url,
allow_post_probe: bool,
) -> Result<Option<Url>, AuthError> {
async fn probe_resource_metadata_url(&self, url: &Url) -> Option<Url> {
let response = match self.discovery_get(url).await {
Ok(r) => r,
Err(e) => {
debug!("resource metadata probe failed: {}", e);
return Ok(None);
return None;
}
};
match response.status() {
StatusCode::OK => Ok(Some(url.clone())),
StatusCode::UNAUTHORIZED => Ok(self
.extract_resource_metadata_url_from_www_authenticate(&response)
.await),
StatusCode::NOT_FOUND | StatusCode::METHOD_NOT_ALLOWED if allow_post_probe => {
self.fetch_resource_metadata_url_with_post_probe(url).await
StatusCode::OK => Some(url.clone()),
StatusCode::UNAUTHORIZED => {
self.extract_resource_metadata_url_from_www_authenticate(&response)
.await
}
status => {
debug!("resource metadata probe returned unexpected status: {status}");
Ok(None)
None
}
}
}
async fn fetch_resource_metadata_url_with_post_probe(
&self,
url: &Url,
) -> Result<Option<Url>, AuthError> {
let request = oauth2::http::Request::builder()
.method("POST")
.uri(url.as_str())
.header(HEADER_MCP_PROTOCOL_VERSION, "2024-11-05")
.header(CONTENT_TYPE, "application/json")
.body(RESOURCE_METADATA_POST_PROBE_BODY.as_bytes().to_vec())
.map_err(|error| AuthError::InternalError(error.to_string()))?;
let response = match self
.http_client
.execute(OAuthHttpRequest::new(
request,
OAuthHttpRedirectPolicy::Stop,
))
.await
{
Ok(response) => response,
Err(error) => {
debug!("resource metadata POST probe failed: {}", error);
return Ok(None);
}
};
if response.status() != StatusCode::UNAUTHORIZED {
debug!(
"resource metadata POST probe returned unexpected status: {}",
response.status()
);
return Ok(None);
}
Ok(self
.extract_resource_metadata_url_from_www_authenticate(&response)
.await)
}
async fn extract_resource_metadata_url_from_www_authenticate(
&self,
response: &HttpResponse,
@ -2549,14 +2570,9 @@ impl AuthorizationManager {
continue;
};
let params = Self::extract_www_authenticate_params(value_str, &self.base_url);
if let Some(url) = params.resource_metadata_url {
if let Some(scope) = &params.scope {
debug!("WWW-Authenticate header contains scope: {}", scope);
let scopes: Vec<String> =
scope.split_whitespace().map(|s| s.to_string()).collect();
*self.www_auth_scopes.write().await = scopes;
}
parsed_url = Some(url);
if params.resource_metadata_url.is_some() {
self.record_challenge_scope(&params).await;
parsed_url = params.resource_metadata_url;
break;
}
}
@ -3432,7 +3448,7 @@ impl OAuthState {
)
}
async fn placeholder(&self) -> Result<Self, AuthError> {
async fn placeholder_state(&self) -> Result<Self, AuthError> {
let (http_client, refresh_redirect_policy) = self.oauth_http_client_config();
Ok(OAuthState::Unauthorized(
AuthorizationManager::new_inner(
@ -3548,7 +3564,7 @@ impl OAuthState {
&mut self,
request: AuthorizationRequest,
) -> Result<(), AuthError> {
let placeholder = self.placeholder().await?;
let placeholder = self.placeholder_state().await?;
let old = std::mem::replace(self, placeholder);
let OAuthState::Unauthorized(mut manager) = old else {
*self = old;
@ -3557,7 +3573,10 @@ impl OAuthState {
));
};
debug!("start discovery");
let metadata = match manager.resolve_metadata().await {
let resolution = manager
.resolve_metadata_from_challenge(request.challenge.as_deref())
.await;
let metadata = match resolution {
Ok(resolution) => resolution.metadata,
Err(e) => {
*self = OAuthState::Unauthorized(manager);
@ -3580,7 +3599,7 @@ impl OAuthState {
/// complete authorization
pub async fn complete_authorization(&mut self) -> Result<(), AuthError> {
let placeholder = self.placeholder().await?;
let placeholder = self.placeholder_state().await?;
if let OAuthState::Session(session) = std::mem::replace(self, placeholder) {
*self = OAuthState::Authorized(session.auth_manager);
Ok(())
@ -3590,7 +3609,7 @@ impl OAuthState {
}
/// covert to authorized http client
pub async fn to_authorized_http_client(&mut self) -> Result<(), AuthError> {
let placeholder = self.placeholder().await?;
let placeholder = self.placeholder_state().await?;
if let OAuthState::Authorized(manager) = std::mem::replace(self, placeholder) {
*self = OAuthState::AuthorizedHttpClient(AuthorizedHttpClient::new(
Arc::new(manager),
@ -3610,7 +3629,7 @@ impl OAuthState {
required_scope: &str,
redirect_uri: &str,
) -> Result<String, AuthError> {
let placeholder = self.placeholder().await?;
let placeholder = self.placeholder_state().await?;
let old = std::mem::replace(self, placeholder);
let OAuthState::Authorized(manager) = old else {
*self = old;
@ -3742,7 +3761,7 @@ impl OAuthState {
&mut self,
config: ClientCredentialsConfig,
) -> Result<(), AuthError> {
let placeholder = self.placeholder().await?;
let placeholder = self.placeholder_state().await?;
let OAuthState::Unauthorized(mut manager) = std::mem::replace(self, placeholder) else {
return Err(AuthError::InternalError(
"Client credentials flow requires Unauthorized state".to_string(),
@ -4137,7 +4156,6 @@ mod tests {
.body(Vec::new())
.unwrap();
let client = RecordingOAuthHttpClient::with_responses(vec![
empty_response(404),
challenge,
http_response(
200,
@ -4178,7 +4196,6 @@ mod tests {
(
"https://auth.example.com/tenant1/token",
vec![
"https://mcp.example.com/mcp",
"https://mcp.example.com/mcp",
"https://mcp.example.com/custom/metadata/location.json",
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1",
@ -4187,14 +4204,12 @@ mod tests {
],
)
);
assert_eq!(
assert!(
client
.requests()
.iter()
.take(2)
.map(|request| request.method.as_str())
.collect::<Vec<_>>(),
vec!["GET", "POST"]
.all(|request| request.method == "GET"),
"discovery must not send non-GET requests"
);
}
@ -4205,7 +4220,6 @@ mod tests {
empty_response(404),
empty_response(404),
empty_response(404),
empty_response(404),
]);
let manager = AuthorizationManager::new_with_oauth_http_client(
"https://legacy.example.com/",
@ -4234,7 +4248,6 @@ mod tests {
"https://legacy.example.com/token",
Some("https://legacy.example.com/register"),
vec![
"https://legacy.example.com/",
"https://legacy.example.com/",
"https://legacy.example.com/.well-known/oauth-protected-resource",
"https://legacy.example.com/.well-known/oauth-authorization-server",
@ -4293,6 +4306,85 @@ mod tests {
);
}
#[tokio::test]
async fn resolve_metadata_from_challenge_uses_challenge_pointer_and_scope() {
let client = RecordingOAuthHttpClient::with_responses(vec![
http_response(
200,
serde_json::json!({
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"]
}),
),
http_response(
200,
serde_json::json!({
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token"
}),
),
]);
let manager = AuthorizationManager::new_with_oauth_http_client(
"https://mcp.example.com/mcp",
Arc::new(client.clone()),
)
.await
.unwrap();
let resolution = manager
.resolve_metadata_from_challenge(Some(
r#"Bearer resource_metadata="https://mcp.example.com/custom/prm.json", scope="mcp:read mcp:write""#,
))
.await
.unwrap();
assert_eq!(
(
resolution.source,
resolution.metadata.token_endpoint.as_str(),
manager.select_scopes(None, &[]),
client.requests().first().map(|request| request.uri.clone()),
),
(
AuthorizationMetadataSource::ProtectedResourceMetadata,
"https://auth.example.com/token",
vec!["mcp:read".to_string(), "mcp:write".to_string()],
// discovery starts at the challenge's pointer: no probing
Some("https://mcp.example.com/custom/prm.json".to_string()),
)
);
}
#[tokio::test]
async fn resolve_metadata_from_challenge_falls_back_without_metadata_pointer() {
let client = RecordingOAuthHttpClient::with_responses(vec![
empty_response(404),
empty_response(404),
empty_response(404),
empty_response(404),
]);
let manager = AuthorizationManager::new_with_oauth_http_client(
"https://legacy.example.com/",
Arc::new(client),
)
.await
.unwrap();
let resolution = manager
.resolve_metadata_from_challenge(Some(r#"Bearer scope="mcp:basic""#))
.await
.unwrap();
assert_eq!(
(resolution.source, manager.select_scopes(None, &[]),),
(
AuthorizationMetadataSource::LegacyEndpointFallback,
vec!["mcp:basic".to_string()],
)
);
}
#[tokio::test]
async fn resolve_metadata_reports_authorization_server_metadata() {
let client = RecordingOAuthHttpClient::with_responses(vec![

View file

@ -1,11 +1,70 @@
use std::collections::HashMap;
use http::{HeaderName, HeaderValue};
use tracing::debug;
use crate::transport::{
auth::AuthClient,
auth::{AuthClient, AuthError},
streamable_http_client::{StreamableHttpClient, StreamableHttpError},
};
impl<C> AuthClient<C>
where
C: StreamableHttpClient + Send + Sync,
{
/// Run `call` with a token when one is available, reacting to the
/// server's auth verdict instead of requiring credentials up front:
///
/// - no usable credentials → the request goes out unauthenticated, and a
/// 401 propagates as [`StreamableHttpError::AuthRequired`] carrying the
/// `WWW-Authenticate` challenge for the caller to authorize with;
/// - a token the server rejects (e.g. revoked) → one silent refresh, one
/// retry, then the challenge propagates.
async fn call_reacting_to_challenges<T, F, Fut>(
&self,
auth_token: Option<String>,
call: F,
) -> Result<T, StreamableHttpError<C::Error>>
where
F: Fn(Option<String>) -> Fut,
Fut: Future<Output = Result<T, StreamableHttpError<C::Error>>>,
{
// Missing credentials are not an error in the reactive model: the
// request goes out unauthenticated and the server's 401 challenge
// drives authorization.
let auth_token = match auth_token {
None => match self.get_access_token().await {
Ok(token) => Some(token),
Err(AuthError::AuthorizationRequired) => None,
Err(error) => return Err(error.into()),
},
token => token,
};
match call(auth_token.clone()).await {
Err(StreamableHttpError::AuthRequired(challenge)) => {
// One silent recovery attempt: refresh the rejected token and
// retry only when the refresh actually produced a new one.
let Some(sent_token) = auth_token else {
return Err(StreamableHttpError::AuthRequired(challenge));
};
let refreshed = {
let manager = self.auth_manager.lock().await;
manager.try_refresh_or_reauth().await
};
match refreshed {
Ok(fresh_token) if fresh_token != sent_token => call(Some(fresh_token)).await,
Ok(_) => Err(StreamableHttpError::AuthRequired(challenge)),
Err(error) => {
debug!("token refresh after server rejection failed: {error}");
Err(StreamableHttpError::AuthRequired(challenge))
}
}
}
result => result,
}
}
}
impl<C> StreamableHttpClient for AuthClient<C>
where
C: StreamableHttpClient + Send + Sync,
@ -16,16 +75,21 @@ where
&self,
uri: std::sync::Arc<str>,
session_id: std::sync::Arc<str>,
mut auth_token: Option<String>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
) -> Result<(), crate::transport::streamable_http_client::StreamableHttpError<Self::Error>>
{
if auth_token.is_none() {
auth_token = Some(self.get_access_token().await?);
}
self.http_client
.delete_session(uri, session_id, auth_token, custom_headers)
.await
self.call_reacting_to_challenges(auth_token, |token| {
let uri = uri.clone();
let session_id = session_id.clone();
let custom_headers = custom_headers.clone();
async move {
self.http_client
.delete_session(uri, session_id, token, custom_headers)
.await
}
})
.await
}
async fn get_stream(
@ -33,18 +97,24 @@ where
uri: std::sync::Arc<str>,
session_id: Option<std::sync::Arc<str>>,
last_event_id: Option<String>,
mut auth_token: Option<String>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
) -> Result<
futures::stream::BoxStream<'static, Result<sse_stream::Sse, sse_stream::Error>>,
crate::transport::streamable_http_client::StreamableHttpError<Self::Error>,
> {
if auth_token.is_none() {
auth_token = Some(self.get_access_token().await?);
}
self.http_client
.get_stream(uri, session_id, last_event_id, auth_token, custom_headers)
.await
self.call_reacting_to_challenges(auth_token, |token| {
let uri = uri.clone();
let session_id = session_id.clone();
let last_event_id = last_event_id.clone();
let custom_headers = custom_headers.clone();
async move {
self.http_client
.get_stream(uri, session_id, last_event_id, token, custom_headers)
.await
}
})
.await
}
async fn get_stream_with_max_sse_event_size(
@ -52,26 +122,32 @@ where
uri: std::sync::Arc<str>,
session_id: Option<std::sync::Arc<str>>,
last_event_id: Option<String>,
mut auth_token: Option<String>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
max_sse_event_size: usize,
) -> Result<
futures::stream::BoxStream<'static, Result<sse_stream::Sse, sse_stream::Error>>,
crate::transport::streamable_http_client::StreamableHttpError<Self::Error>,
> {
if auth_token.is_none() {
auth_token = Some(self.get_access_token().await?);
}
self.http_client
.get_stream_with_max_sse_event_size(
uri,
session_id,
last_event_id,
auth_token,
custom_headers,
max_sse_event_size,
)
.await
self.call_reacting_to_challenges(auth_token, |token| {
let uri = uri.clone();
let session_id = session_id.clone();
let last_event_id = last_event_id.clone();
let custom_headers = custom_headers.clone();
async move {
self.http_client
.get_stream_with_max_sse_event_size(
uri,
session_id,
last_event_id,
token,
custom_headers,
max_sse_event_size,
)
.await
}
})
.await
}
async fn post_message(
@ -79,18 +155,24 @@ where
uri: std::sync::Arc<str>,
message: crate::model::ClientJsonRpcMessage,
session_id: Option<std::sync::Arc<str>>,
mut auth_token: Option<String>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
) -> Result<
crate::transport::streamable_http_client::StreamableHttpPostResponse,
StreamableHttpError<Self::Error>,
> {
if auth_token.is_none() {
auth_token = Some(self.get_access_token().await?);
}
self.http_client
.post_message(uri, message, session_id, auth_token, custom_headers)
.await
self.call_reacting_to_challenges(auth_token, |token| {
let uri = uri.clone();
let message = message.clone();
let session_id = session_id.clone();
let custom_headers = custom_headers.clone();
async move {
self.http_client
.post_message(uri, message, session_id, token, custom_headers)
.await
}
})
.await
}
async fn post_message_with_max_sse_event_size(
@ -98,25 +180,31 @@ where
uri: std::sync::Arc<str>,
message: crate::model::ClientJsonRpcMessage,
session_id: Option<std::sync::Arc<str>>,
mut auth_token: Option<String>,
auth_token: Option<String>,
custom_headers: HashMap<HeaderName, HeaderValue>,
max_sse_event_size: usize,
) -> Result<
crate::transport::streamable_http_client::StreamableHttpPostResponse,
StreamableHttpError<Self::Error>,
> {
if auth_token.is_none() {
auth_token = Some(self.get_access_token().await?);
}
self.http_client
.post_message_with_max_sse_event_size(
uri,
message,
session_id,
auth_token,
custom_headers,
max_sse_event_size,
)
.await
self.call_reacting_to_challenges(auth_token, |token| {
let uri = uri.clone();
let message = message.clone();
let session_id = session_id.clone();
let custom_headers = custom_headers.clone();
async move {
self.http_client
.post_message_with_max_sse_event_size(
uri,
message,
session_id,
token,
custom_headers,
max_sse_event_size,
)
.await
}
})
.await
}
}

View file

@ -124,7 +124,8 @@ fn negotiate_version_headers(
(version, headers)
}
#[derive(Debug)]
#[derive(Debug, Error)]
#[error("authorization required: {www_authenticate_header}")]
#[non_exhaustive]
pub struct AuthRequiredError {
pub www_authenticate_header: String,
@ -139,7 +140,8 @@ impl AuthRequiredError {
}
}
#[derive(Debug)]
#[derive(Debug, Error)]
#[error("insufficient scope: {www_authenticate_header}")]
#[non_exhaustive]
pub struct InsufficientScopeError {
pub www_authenticate_header: String,
@ -197,15 +199,31 @@ pub enum StreamableHttpError<E: std::error::Error + Send + Sync + 'static> {
#[error("Auth error: {0}")]
Auth(#[from] crate::transport::auth::AuthError),
#[error("Auth required")]
AuthRequired(AuthRequiredError),
AuthRequired(#[source] AuthRequiredError),
#[error("Insufficient scope")]
InsufficientScope(InsufficientScopeError),
InsufficientScope(#[source] InsufficientScopeError),
#[error("Header name '{0}' is reserved and conflicts with default headers")]
ReservedHeaderConflict(String),
#[error("Session expired (HTTP 404)")]
SessionExpired,
}
impl<E: std::error::Error + Send + Sync + 'static> StreamableHttpError<E> {
/// The `WWW-Authenticate` challenge carried by this error, when the
/// server answered 401 ([`AuthRequired`](Self::AuthRequired)) or 403
/// ([`InsufficientScope`](Self::InsufficientScope)). Feed it to
/// [`AuthorizationRequest::with_challenge`](crate::transport::auth::AuthorizationRequest::with_challenge)
/// to authorize reactively.
#[cfg(feature = "auth")]
pub fn auth_challenge(&self) -> Option<&str> {
match self {
Self::AuthRequired(error) => Some(&error.www_authenticate_header),
Self::InsufficientScope(error) => Some(&error.www_authenticate_header),
_ => None,
}
}
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum StreamableHttpProtocolError {