use std::fmt;
use bevy_ecs::message::Message;
use http::header::{HeaderMap, HeaderName, HeaderValue};
use http::StatusCode;
use crate::RequestId;
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RawResponse {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Vec<u8>,
}
impl fmt::Debug for RawResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let headers: Vec<&str> = self.headers.keys().map(HeaderName::as_str).collect();
f.debug_struct("RawResponse").field("status", &self.status).field("header_names", &headers).field("body_bytes", &self.body.len()).finish()
}
}
impl RawResponse {
pub fn new(status: StatusCode, body: impl Into<Vec<u8>>) -> Self {
Self { status, headers: HeaderMap::new(), body: body.into() }
}
pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.headers.append(name, value);
self
}
pub fn is_success(&self) -> bool {
self.status.is_success()
}
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
if self.body.iter().all(u8::is_ascii_whitespace) {
serde_json::from_slice(b"null")
} else {
serde_json::from_slice(&self.body)
}
}
}
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BackendError {
InvalidRequest(String),
#[non_exhaustive]
InsecureHttp {
host: String,
},
Encode(String),
Network(String),
Tls(String),
Timeout(String),
#[non_exhaustive]
BodyTooLarge {
limit: u64,
},
Status(Box<RawResponse>),
#[non_exhaustive]
Decode {
message: String,
response: Box<RawResponse>,
},
Cancelled,
Shutdown,
NoTransport,
#[non_exhaustive]
Disconnected {
reason: String,
sent: Option<bool>,
},
#[non_exhaustive]
Closed {
code: u16,
reason: String,
},
#[non_exhaustive]
RequestTooLarge {
limit: u64,
size: u64,
},
Rejected(Box<Rejection>),
#[non_exhaustive]
HostKey {
host: String,
fingerprint: String,
problem: HostKeyProblem,
},
AuthFailed(String),
Ssh(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum HostKeyProblem {
Unknown,
Changed,
Revoked,
}
impl fmt::Display for HostKeyProblem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
HostKeyProblem::Unknown => "unknown host key",
HostKeyProblem::Changed => "the host key CHANGED",
HostKeyProblem::Revoked => "the host key is REVOKED",
})
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Rejection {
payload: Vec<u8>,
}
impl fmt::Debug for Rejection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rejection").field("payload_bytes", &self.payload.len()).finish()
}
}
impl Rejection {
pub fn new(payload: impl Into<Vec<u8>>) -> Self {
Self { payload: payload.into() }
}
pub fn bytes(&self) -> &[u8] {
&self.payload
}
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.payload).into_owned()
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_slice(&self.payload)
}
}
impl BackendError {
pub fn status(&self) -> Option<StatusCode> {
self.response().map(|r| r.status)
}
pub fn response(&self) -> Option<&RawResponse> {
match self {
BackendError::Status(response) | BackendError::Decode { response, .. } => Some(response.as_ref()),
_ => None,
}
}
pub fn is_invalid_request(&self) -> bool {
matches!(self, BackendError::InvalidRequest(_) | BackendError::InsecureHttp { .. } | BackendError::Encode(_) | BackendError::RequestTooLarge { .. })
}
pub fn was_sent(&self) -> Option<bool> {
match self {
BackendError::InvalidRequest(_) | BackendError::InsecureHttp { .. } | BackendError::Encode(_) | BackendError::RequestTooLarge { .. } => Some(false),
BackendError::HostKey { .. } | BackendError::AuthFailed(_) => Some(false),
BackendError::Timeout(why) if why.starts_with("not sent:") => Some(false),
BackendError::Disconnected { sent, .. } => *sent,
BackendError::Status(_) | BackendError::Decode { .. } | BackendError::BodyTooLarge { .. } | BackendError::Rejected(_) => Some(true),
_ => None,
}
}
pub fn close_code(&self) -> Option<u16> {
match self {
BackendError::Closed { code, .. } => Some(*code),
_ => None,
}
}
pub fn host_key(host: impl Into<String>, fingerprint: impl Into<String>, problem: HostKeyProblem) -> Self {
BackendError::HostKey { host: host.into(), fingerprint: fingerprint.into(), problem }
}
pub fn request_too_large(limit: u64, size: u64) -> Self {
BackendError::RequestTooLarge { limit, size }
}
#[cfg(any(feature = "ws", feature = "ssh"))]
pub(crate) fn disconnected(reason: impl Into<String>, sent: Option<bool>) -> Self {
BackendError::Disconnected { reason: reason.into(), sent }
}
}
impl fmt::Debug for BackendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BackendError::InvalidRequest(why) => f.debug_tuple("InvalidRequest").field(why).finish(),
BackendError::InsecureHttp { host } => f.debug_struct("InsecureHttp").field("host", host).finish(),
BackendError::Encode(why) => f.debug_tuple("Encode").field(why).finish(),
BackendError::Network(why) => f.debug_tuple("Network").field(why).finish(),
BackendError::Tls(why) => f.debug_tuple("Tls").field(why).finish(),
BackendError::Timeout(why) => f.debug_tuple("Timeout").field(why).finish(),
BackendError::BodyTooLarge { limit } => f.debug_struct("BodyTooLarge").field("limit", limit).finish(),
BackendError::RequestTooLarge { limit, size } => f.debug_struct("RequestTooLarge").field("limit", limit).field("size", size).finish(),
BackendError::Status(response) => f.debug_tuple("Status").field(response).finish(),
BackendError::Decode { message, response } => f.debug_struct("Decode").field("message_len", &message.len()).field("response", response).finish(),
BackendError::Cancelled => f.write_str("Cancelled"),
BackendError::Shutdown => f.write_str("Shutdown"),
BackendError::NoTransport => f.write_str("NoTransport"),
BackendError::Disconnected { reason, sent } => f.debug_struct("Disconnected").field("reason", reason).field("sent", sent).finish(),
BackendError::Closed { code, reason } => f.debug_struct("Closed").field("code", code).field("reason", reason).finish(),
BackendError::Rejected(rejection) => f.debug_tuple("Rejected").field(rejection).finish(),
BackendError::HostKey { host, fingerprint, problem } => {
f.debug_struct("HostKey").field("host", host).field("fingerprint", fingerprint).field("problem", problem).finish()
}
BackendError::AuthFailed(why) => f.debug_tuple("AuthFailed").field(why).finish(),
BackendError::Ssh(why) => f.debug_tuple("Ssh").field(why).finish(),
}
}
}
impl fmt::Display for BackendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BackendError::InvalidRequest(why) => write!(f, "invalid request: {why}"),
BackendError::InsecureHttp { host } => {
write!(f, "plain http:// to `{host}` refused (use https://, a loopback host, or allow_insecure_http)")
}
BackendError::Encode(why) => write!(f, "could not encode the JSON body: {why}"),
BackendError::Network(why) => write!(f, "network error: {why}"),
BackendError::Tls(why) => write!(f, "TLS error: {why}"),
BackendError::Timeout(why) => write!(f, "timed out ({why})"),
BackendError::BodyTooLarge { limit } => write!(f, "the answer is larger than the limit of {limit} bytes"),
BackendError::RequestTooLarge { limit, size } => write!(f, "the request ({size} bytes) is larger than the limit of {limit} bytes; not sent"),
BackendError::Status(response) => write!(f, "HTTP status {}", response.status),
BackendError::Decode { response, .. } => write!(f, "the answer (HTTP {}) is not the expected JSON", response.status),
BackendError::Cancelled => f.write_str("cancelled"),
BackendError::Shutdown => f.write_str("the app is shutting down"),
BackendError::NoTransport => f.write_str("no transport is installed (or it was replaced before it answered)"),
BackendError::Disconnected { reason, sent: Some(true) } => write!(f, "disconnected after the request was sent: {reason}"),
BackendError::Disconnected { reason, sent: Some(false) } => write!(f, "disconnected, the request was never sent: {reason}"),
BackendError::Disconnected { reason, sent: None } => write!(f, "disconnected: {reason}"),
BackendError::Closed { code, reason } if reason.is_empty() => write!(f, "closed by the server (code {code})"),
BackendError::Closed { code, reason } => write!(f, "closed by the server (code {code}: {reason})"),
BackendError::Rejected(_) => f.write_str("the server rejected the request"),
BackendError::HostKey { host, fingerprint, problem } => write!(f, "SSH host key check failed for `{host}`: {problem} ({fingerprint})"),
BackendError::AuthFailed(why) => write!(f, "SSH authentication failed: {why}"),
BackendError::Ssh(why) => write!(f, "SSH error: {why}"),
}
}
}
impl std::error::Error for BackendError {}
#[derive(Message, Clone, Debug)]
#[non_exhaustive]
pub struct HttpResponse {
pub id: RequestId,
pub result: Result<RawResponse, BackendError>,
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
#[derive(Message, Clone, Debug)]
#[non_exhaustive]
pub struct JsonResponse<T: Send + Sync + 'static> {
pub id: RequestId,
pub result: Result<T, BackendError>,
}