feat(transport): add constructors for non_exhaustive error types (#806)

AuthRequiredError, InsufficientScopeError, and DynamicTransportError
were marked #[non_exhaustive] in #715/#768 but don't have constructors
usable by external crates. Add new() for the error types and
from_parts() for DynamicTransportError (the existing new() requires a
Transport type parameter, making it unusable for test fixtures).

Fixes #805
This commit is contained in:
Will Pfleger 2026-04-13 16:31:26 -04:00 committed by GitHub
parent a743f15654
commit ad3997268d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 9 deletions

View file

@ -252,6 +252,24 @@ impl DynamicTransportError {
error: Box::new(e),
}
}
/// Create a `DynamicTransportError` from raw parts.
///
/// Unlike [`new`](Self::new), this does not require a concrete [`Transport`] type,
/// making it usable in test fixtures and other contexts where a real transport
/// implementation is not available.
pub fn from_parts(
transport_name: impl Into<Cow<'static, str>>,
transport_type_id: std::any::TypeId,
error: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self {
transport_name: transport_name.into(),
transport_type_id,
error,
}
}
pub fn downcast<T: Transport<R> + 'static, R: ServiceRole>(self) -> Result<T::Error, Self> {
if !self.is::<T, R>() {
Err(self)

View file

@ -284,21 +284,28 @@ impl StreamableHttpClientTransport<reqwest::Client> {
#[cfg(test)]
mod tests {
use super::parse_json_rpc_error;
use crate::{model::JsonRpcMessage, transport::streamable_http_client::InsufficientScopeError};
use crate::{
model::JsonRpcMessage,
transport::streamable_http_client::{AuthRequiredError, InsufficientScopeError},
};
#[test]
fn auth_required_error_new() {
let err = AuthRequiredError::new("Bearer realm=\"test\"".to_string());
assert_eq!(err.www_authenticate_header, "Bearer realm=\"test\"");
}
#[test]
fn insufficient_scope_error_can_upgrade() {
let with_scope = InsufficientScopeError {
www_authenticate_header: "Bearer scope=\"admin\"".to_string(),
required_scope: Some("admin".to_string()),
};
let with_scope = InsufficientScopeError::new(
"Bearer scope=\"admin\"".to_string(),
Some("admin".to_string()),
);
assert!(with_scope.can_upgrade());
assert_eq!(with_scope.get_required_scope(), Some("admin"));
let without_scope = InsufficientScopeError {
www_authenticate_header: "Bearer error=\"insufficient_scope\"".to_string(),
required_scope: None,
};
let without_scope =
InsufficientScopeError::new("Bearer error=\"insufficient_scope\"".to_string(), None);
assert!(!without_scope.can_upgrade());
assert_eq!(without_scope.get_required_scope(), None);
}

View file

@ -29,6 +29,15 @@ pub struct AuthRequiredError {
pub www_authenticate_header: String,
}
impl AuthRequiredError {
/// Create a new `AuthRequiredError` instance.
pub fn new(www_authenticate_header: String) -> Self {
Self {
www_authenticate_header,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct InsufficientScopeError {
@ -37,6 +46,14 @@ pub struct InsufficientScopeError {
}
impl InsufficientScopeError {
/// Create a new `InsufficientScopeError` instance.
pub fn new(www_authenticate_header: String, required_scope: Option<String>) -> Self {
Self {
www_authenticate_header,
required_scope,
}
}
/// check if scope upgrade is possible (i.e., we know what scope is required)
pub fn can_upgrade(&self) -> bool {
self.required_scope.is_some()