use crate::adapters::primary::transport::http::SseError;
use crate::domain::McpSessionIdError;
const REFUSAL_PREVIEW: usize = 256;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HttpClientError {
#[error("the {method} request to the peer could not be completed: {error}")]
Io {
method: &'static str,
error: reqwest::Error,
},
#[error("the peer refused the {method} with {status}: {preview}")]
Refused {
method: &'static str,
status: reqwest::StatusCode,
content_type: Option<String>,
preview: String,
},
#[error("the peer answered 2xx with {got:?}, which is neither JSON nor an event stream")]
MediaType {
got: String,
},
#[error("the peer answered 2xx with no content type")]
MissingMediaType,
#[error("the peer answered 2xx with a JSON body that is not an object: {got:?}")]
NotAMessage {
got: String,
},
#[error("the {method} stream closed before the answer arrived")]
Unanswered {
method: &'static str,
},
#[error("authorization failed: {0}")]
Authorization(String),
#[error("the {method} body could not be {what}: {detail}")]
Body {
method: &'static str,
what: &'static str,
detail: String,
},
#[error("the event stream produced a frame this client cannot read: {source}")]
Frame {
source: SseError,
},
#[error("the event stream ended after {delivered} delivered event(s)")]
StreamEnded {
delivered: u64,
},
#[error(
"the stream was not completed after {attempts} reconnect attempt(s), having delivered {delivered}; last failure: {source}"
)]
ResumeExhausted {
attempts: u32,
delivered: u64,
source: Box<Self>,
},
#[error("this call needs a session id and the peer has not issued one yet")]
NoSessionYet,
#[error("the peer sent an Mcp-Session-Id that is not a session id: {source}")]
SessionId {
source: McpSessionIdError,
},
#[error("the endpoint {got:?} is not a usable URL: {reason}")]
Url {
got: String,
reason: String,
},
}
impl HttpClientError {
#[must_use]
pub const fn is_transient(&self) -> bool {
matches!(self, Self::Io { .. } | Self::StreamEnded { .. })
}
#[must_use]
pub const fn status(&self) -> Option<reqwest::StatusCode> {
match self {
Self::Refused { status, .. } => Some(*status),
_ => None,
}
}
}
pub(super) fn preview(bytes: &[u8]) -> String {
let shown = bytes.get(..REFUSAL_PREVIEW).unwrap_or(bytes);
String::from_utf8_lossy(shown)
.chars()
.take(REFUSAL_PREVIEW)
.collect()
}