#[non_exhaustive]pub enum HttpClientError {
Show 14 variants
Io {
method: &'static str,
error: Error,
},
Refused {
method: &'static str,
status: StatusCode,
content_type: Option<String>,
preview: String,
},
MediaType {
got: String,
},
MissingMediaType,
NotAMessage {
got: String,
},
Unanswered {
method: &'static str,
},
Authorization(String),
Body {
method: &'static str,
what: &'static str,
detail: String,
},
Frame {
source: SseError,
},
StreamEnded {
delivered: u64,
},
ResumeExhausted {
attempts: u32,
delivered: u64,
source: Box<Self>,
},
NoSessionYet,
SessionId {
source: McpSessionIdError,
},
Url {
got: String,
reason: String,
},
}Expand description
Why a Streamable HTTP call did not produce a response the caller can use.
§Examples
Every variant is distinguishable, which is the reason this transport maps
into one enum instead of letting a reqwest::Error through:
use protocol_droid::adapters::secondary::transport::http::HttpClientError;
// A refusal and a dead socket are different problems for a retry policy:
// one is the peer's answer, the other is a network worth waiting on. Both
// are `Err`, and a caller that treats `Err` as "retry" cannot tell them
// apart without matching on the variant.
let error = HttpClientError::MediaType { got: "text/html".into() };
assert!(matches!(error, HttpClientError::MediaType { .. }));
assert!(!matches!(error, HttpClientError::Io { .. }));Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Io
The request never reached the peer, or the response never came back.
The only variant a caller should retry without thinking — and even then
only for an idempotent request. A POST that may have been received and
acted on before the socket failed is exactly the case the idempotency
key exists for, and retrying it blind is how a tool runs twice.
Fields
Refused
The peer answered with a status outside 2xx.
Fields
status: StatusCodeThe status the peer answered with.
MediaType
The peer answered 2xx with a body this client cannot read.
Kept apart from HttpClientError::Refused on purpose. A 200 with
the wrong content type is the peer and the client disagreeing about what
a response is, and no retry fixes that — a policy that treated it as a
transport blip would loop against a server that answers identically
every time.
MissingMediaType
The peer answered 2xx with no Content-Type at all.
NotAMessage
The peer answered 2xx with a body that is not a JSON object.
Its own variant, not HttpClientError::MediaType: that one means “not
JSON or not an event stream” and leaves the caller to pick which. Here
the peer committed to JSON and then sent something that is not a
JSON-RPC message, which is a different fault with a different fix.
Unanswered
The peer’s stream closed without the answer this request was waiting on.
Its own variant because a stream that ends is a different fault from one that never began: the first means the peer accepted the request and stopped talking, and a bridge reporting it as a transport error tells the client something true about a request the server did receive.
Authorization(String)
The server demanded authorization and the client could not provide it.
Carries the authorizer’s own account, which never contains the token.
Body
A body could not be encoded for sending, or a reply body could not be read back as JSON.
Separate from HttpClientError::Io because that one carries a
reqwest::Error and means the request never reached the peer. This is
the opposite: the bytes arrived and could not be understood. Folding
both into Io would report a marshalling fault as a network fault.
Fields
Frame
The event stream produced bytes that are not a valid SSE frame.
StreamEnded
The event stream ended, or the socket broke, mid-frame.
Not terminal on its own: a resumable read treats this as “reconnect
from the last delivered id” and only surfaces it once the reconnect
budget is gone. See HttpClientError::ResumeExhausted.
Fields
ResumeExhausted
A resumable read ran out of reconnects.
Fields
NoSessionYet
A call that requires a session was made before one had been issued.
SessionId
The peer sent a Mcp-Session-Id this client cannot parse.
Refused rather than ignored: a client that silently drops an unparseable session id and carries on sends its next request with no session header, and the resulting failure points at the session layer instead of at the header that was actually wrong.
Fields
source: McpSessionIdErrorWhat was wrong with it.
Url
The endpoint is not a URL a request can be built against.
Implementations§
Source§impl HttpClientError
impl HttpClientError
Sourcepub const fn is_transient(&self) -> bool
pub const fn is_transient(&self) -> bool
Whether retrying the identical request could plausibly succeed.
A convenience, not a policy: it says what the error implies, and the
caller’s idempotency story still decides whether a retry is safe. A
POST that may already have run is false even when the socket died,
because the honest answer there is “ask the idempotency store”, not
“send it again”.
Sourcepub const fn status(&self) -> Option<StatusCode>
pub const fn status(&self) -> Option<StatusCode>
The status the peer answered with, when there was one.
None for every variant where the request never got an answer, which is
the distinction a caller’s logging actually needs.
Trait Implementations§
Source§impl Debug for HttpClientError
impl Debug for HttpClientError
Source§impl Display for HttpClientError
impl Display for HttpClientError
Source§impl Error for HttpClientError
impl Error for HttpClientError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<FrameError> for HttpClientError
Maps a frame failure onto the one enum a caller above this layer matches.
impl From<FrameError> for HttpClientError
Maps a frame failure onto the one enum a caller above this layer matches.
BlockTooLong is the one variant that does not come straight from the
shared grammar, so it says what actually happened rather than borrowing a
codec error that would name the wrong thing.