use facet::Facet;
use http::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error as ThisError;
use crate::Result;
#[derive(Facet, Serialize, Deserialize, PartialEq, Eq, Clone, ThisError, Debug)]
#[repr(C)]
pub enum HttpError {
#[error("URL parse error: {0}")]
Url(String),
#[error("IO error: {0}")]
Io(String),
#[error("Timeout")]
Timeout,
#[error("HTTP error {code}: {message}")]
#[serde(skip)]
#[facet(skip)]
#[non_exhaustive]
Http {
code: u16,
message: String,
#[facet(opaque)]
headers: Box<HeaderMap>,
body: Vec<u8>,
},
#[error("JSON serialization error: {0}")]
#[serde(skip)]
#[facet(skip)]
Json(String),
#[error("response body had already been taken")]
#[serde(skip)]
#[facet(skip)]
BodyAlreadyTaken,
#[error("invalid HTTP status code: {0}")]
#[serde(skip)]
#[facet(skip)]
InvalidStatusCode(u16),
}
impl HttpError {
#[must_use]
pub const fn code(&self) -> Option<u16> {
match self {
Self::Http { code, .. } => Some(*code),
_ => None,
}
}
#[must_use]
pub fn body(&self) -> Option<&[u8]> {
match self {
Self::Http { body, .. } if !body.is_empty() => Some(body),
_ => None,
}
}
#[must_use]
pub fn header(&self, name: impl http::header::AsHeaderName) -> Option<&HeaderValue> {
self.headers()?.get(name)
}
#[must_use]
pub fn headers(&self) -> Option<&HeaderMap> {
match self {
Self::Http { headers, .. } => Some(headers),
_ => None,
}
}
#[must_use]
pub fn content_type(&self) -> Option<mime::Mime> {
self.header(http::header::CONTENT_TYPE)?
.to_str()
.ok()?
.parse()
.ok()
}
pub fn body_json<T: DeserializeOwned>(&self) -> Result<T> {
let body = self
.body()
.ok_or_else(|| Self::Json("error has no response body".to_string()))?;
serde_json::from_slice(body).map_err(Self::from)
}
}
impl From<std::io::Error> for HttpError {
fn from(e: std::io::Error) -> Self {
Self::Io(e.to_string())
}
}
impl From<serde_json::Error> for HttpError {
fn from(e: serde_json::Error) -> Self {
Self::Json(e.to_string())
}
}
impl From<url::ParseError> for HttpError {
fn from(e: url::ParseError) -> Self {
Self::Url(e.to_string())
}
}
impl From<serde_qs::Error> for HttpError {
fn from(e: serde_qs::Error) -> Self {
Self::Json(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let error = HttpError::Http {
code: 400,
message: "Bad Request".to_string(),
headers: Box::default(),
body: vec![],
};
assert_eq!(error.to_string(), "HTTP error 400: Bad Request");
}
#[test]
fn http_code_is_plain_u16() {
let error = HttpError::Http {
code: 404u16,
message: "Not Found".to_string(),
headers: Box::default(),
body: vec![],
};
assert_eq!(error.to_string(), "HTTP error 404: Not Found");
}
#[test]
fn io_error_converts_to_io_variant() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let http_err = HttpError::from(io_err);
assert!(matches!(http_err, HttpError::Io(_)));
assert_eq!(http_err.to_string(), "IO error: file not found");
}
#[test]
fn serde_json_error_converts_to_json_variant() {
let json_err: serde_json::Error =
serde_json::from_str::<serde_json::Value>("{bad}").unwrap_err();
let http_err = HttpError::from(json_err);
assert!(matches!(http_err, HttpError::Json(_)));
}
#[test]
fn url_parse_error_converts_to_url_variant() {
let url_err = url::Url::parse("not a url").unwrap_err();
let http_err = HttpError::from(url_err);
assert!(matches!(http_err, HttpError::Url(_)));
}
#[test]
fn accessors_read_the_error_response() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let error = HttpError::Http {
code: 409,
message: "409 Conflict".to_string(),
headers: Box::new(headers),
body: br#"{"error":"already booked"}"#.to_vec(),
};
assert_eq!(error.code(), Some(409));
assert_eq!(error.body(), Some(&br#"{"error":"already booked"}"#[..]));
let body: serde_json::Value = error.body_json().expect("body is JSON");
assert_eq!(body["error"], "already booked");
assert_eq!(error.content_type(), Some(mime::APPLICATION_JSON));
assert_eq!(error.header("Content-Type").unwrap(), "application/json");
assert_eq!(error.header("x-absent"), None);
}
#[test]
fn there_are_no_headers_without_a_response() {
assert_eq!(HttpError::Timeout.header("retry-after"), None);
assert_eq!(HttpError::Timeout.headers(), None);
assert_eq!(HttpError::Timeout.content_type(), None);
let error = HttpError::InvalidStatusCode(999);
assert_eq!(error.header("retry-after"), None);
assert_eq!(error.headers(), None);
assert_eq!(error.content_type(), None);
let error = HttpError::Http {
code: 500,
message: "500 Internal Server Error".to_string(),
headers: Box::default(),
body: vec![],
};
assert!(error.headers().expect("a rejection has headers").is_empty());
assert_eq!(error.content_type(), None);
}
#[test]
fn retry_after_survives_on_the_error() {
let error = crate::testing::rejection_from::<Vec<u8>>(
crate::HttpResponse::status(429)
.header("retry-after", "30")
.build(),
)
.expect_err("a 429 is never Ok");
assert_eq!(error.header("retry-after").unwrap(), "30");
}
#[test]
fn accessors_are_empty_for_errors_without_a_response() {
let error = HttpError::Timeout;
assert_eq!(error.code(), None);
assert_eq!(error.body(), None);
assert!(matches!(
error.body_json::<serde_json::Value>(),
Err(HttpError::Json(_))
));
assert_eq!(HttpError::BodyAlreadyTaken.body(), None);
assert_eq!(HttpError::InvalidStatusCode(999).body(), None);
let error = HttpError::Http {
code: 500,
message: "500 Internal Server Error".to_string(),
headers: Box::default(),
body: vec![],
};
assert_eq!(error.code(), Some(500));
assert_eq!(error.body(), None);
}
#[test]
fn only_a_rejection_has_a_code() {
assert_eq!(
crate::testing::rejection::<Vec<u8>>(409, "")
.expect_err("a 409 is never Ok")
.code(),
Some(409)
);
assert_eq!(HttpError::BodyAlreadyTaken.code(), None);
assert_eq!(HttpError::InvalidStatusCode(999).code(), None);
assert_eq!(HttpError::Timeout.code(), None);
assert_eq!(HttpError::Io("refused".to_string()).code(), None);
assert_eq!(HttpError::Url("bad".to_string()).code(), None);
assert_eq!(HttpError::Json("nope".to_string()).code(), None);
}
#[test]
fn the_new_variants_display_usefully() {
assert_eq!(
HttpError::BodyAlreadyTaken.to_string(),
"response body had already been taken"
);
assert_eq!(
HttpError::InvalidStatusCode(999).to_string(),
"invalid HTTP status code: 999"
);
}
#[test]
fn an_empty_body_reads_as_no_body() {
let error = HttpError::Http {
code: 404,
message: "404 Not Found".to_string(),
headers: Box::default(),
body: vec![],
};
assert_eq!(error.body(), None);
}
#[test]
fn body_json_reports_a_body_that_is_not_json() {
let error = HttpError::Http {
code: 502,
message: "502 Bad Gateway".to_string(),
headers: Box::default(),
body: b"<html>nginx</html>".to_vec(),
};
assert!(matches!(
error.body_json::<serde_json::Value>(),
Err(HttpError::Json(_))
));
assert_eq!(error.body(), Some(&b"<html>nginx</html>"[..]));
}
#[test]
fn serde_qs_error_converts_to_json_variant() {
let qs_err: serde_qs::Error =
serde_qs::from_str::<std::collections::HashMap<String, String>>("%bad%").unwrap_err();
let http_err = HttpError::from(qs_err);
assert!(matches!(http_err, HttpError::Json(_)));
}
}