use std::borrow::Cow;
use std::time::Duration;
use http::StatusCode;
use http::header::{InvalidHeaderValue, MaxSizeReached};
use http::method::InvalidMethod;
use http::status::InvalidStatusCode;
use http::uri::{InvalidUri, InvalidUriParts};
use ohno::{ErrorLabel, Labeled};
use recoverable::{Recovery, RecoveryInfo};
use thread_aware::ThreadAware;
use thread_aware::affinity::{MemoryAffinity, PinnedAffinity};
use crate::HttpRequest;
use crate::http_utils::SyncHolder;
const LABEL_HTTP_ERROR: ErrorLabel = ErrorLabel::from_static("http_error");
const LABEL_INVALID_URI_PARTS: ErrorLabel = ErrorLabel::from_static("invalid_uri_parts");
const LABEL_INVALID_URI: ErrorLabel = ErrorLabel::from_static("invalid_uri");
const LABEL_INVALID_HEADER_VALUE: ErrorLabel = ErrorLabel::from_static("invalid_header_value");
const LABEL_INVALID_METHOD: ErrorLabel = ErrorLabel::from_static("invalid_method");
const LABEL_INVALID_STATUS_CODE: ErrorLabel = ErrorLabel::from_static("invalid_status_code");
const LABEL_MAX_SIZE_REACHED: ErrorLabel = ErrorLabel::from_static("max_size_reached");
const LABEL_VALIDATION: ErrorLabel = ErrorLabel::from_static("validation");
const LABEL_UNAVAILABLE: ErrorLabel = ErrorLabel::from_static("unavailable");
const LABEL_TIMEOUT: ErrorLabel = ErrorLabel::from_static("timeout");
pub type Result<T> = std::result::Result<T, HttpError>;
#[ohno::error]
#[from(
http::Error(label: LABEL_HTTP_ERROR, recovery: RecoveryInfo::never()),
InvalidUriParts(label: LABEL_INVALID_URI_PARTS, recovery: RecoveryInfo::never()),
InvalidUri(label: LABEL_INVALID_URI, recovery: RecoveryInfo::never()),
InvalidHeaderValue(label: LABEL_INVALID_HEADER_VALUE, recovery: RecoveryInfo::never()),
InvalidMethod(label: LABEL_INVALID_METHOD, recovery: RecoveryInfo::never()),
InvalidStatusCode(label: LABEL_INVALID_STATUS_CODE, recovery: RecoveryInfo::never()),
MaxSizeReached(label: LABEL_MAX_SIZE_REACHED, recovery: RecoveryInfo::never()),
std::io::Error(label: ErrorLabel::from(error.kind()), recovery: RecoveryInfo::from(error.kind())),
templated_uri::ValidationError(label: LABEL_INVALID_URI, recovery: RecoveryInfo::never())
)]
pub struct HttpError {
label: ErrorLabel,
recovery: RecoveryInfo,
request: Option<SyncHolder<Box<HttpRequest>>>,
}
impl ThreadAware for HttpError {
fn relocated(self, _source: MemoryAffinity, _destination: PinnedAffinity) -> Self {
self
}
}
impl HttpError {
pub fn other(error: impl Into<Box<dyn std::error::Error + Send + Sync>>, recovery: RecoveryInfo, label: impl Into<ErrorLabel>) -> Self {
Self::caused_by(label, recovery, None, error)
}
pub fn other_with_recovery<E>(error: E, label: impl Into<ErrorLabel>) -> Self
where
E: std::error::Error + Send + Sync + Recovery + 'static,
{
let recovery = error.recovery();
Self::other(error, recovery, label)
}
#[must_use]
pub fn invalid_status_code(code: StatusCode, recovery: RecoveryInfo) -> Self {
Self::other(
format!("the response was not successful, status code: {}", code.as_u16()),
recovery,
LABEL_INVALID_STATUS_CODE,
)
}
#[must_use]
pub fn validation(msg: impl Into<Cow<'static, str>>) -> Self {
Self::other(msg.into(), RecoveryInfo::never(), LABEL_VALIDATION)
}
#[must_use]
pub fn unavailable(msg: impl Into<Cow<'static, str>>) -> Self {
Self::other(msg.into(), RecoveryInfo::unavailable(), LABEL_UNAVAILABLE)
}
#[must_use]
pub fn timeout(duration: Duration) -> Self {
Self::other(
format!(
"request timed out while receiving the response, timeout: {}ms",
duration.as_millis()
),
RecoveryInfo::retry(),
LABEL_TIMEOUT,
)
}
#[must_use]
pub(crate) fn timeout_for_body(duration: Duration) -> Self {
Self::other(
format!("body data was not fully received, timeout: {}ms", duration.as_millis()),
RecoveryInfo::retry(),
LABEL_TIMEOUT,
)
}
#[must_use]
pub fn with_request(mut self, request: HttpRequest) -> Self {
self.request = Some(SyncHolder::new(Box::new(request)));
self
}
#[must_use]
pub fn take_request(&mut self) -> Option<HttpRequest> {
self.request.take().map(|holder| *holder.into_inner())
}
}
impl Recovery for HttpError {
fn recovery(&self) -> RecoveryInfo {
self.recovery.clone()
}
}
impl Labeled for HttpError {
fn label(&self) -> &ErrorLabel {
&self.label
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::fmt::{Debug, Display};
use ohno::ErrorExt;
use recoverable::RecoveryKind;
use thread_aware::affinity::pinned_affinities;
use super::*;
use crate::HttpRequestBuilder;
static_assertions::assert_impl_all!(HttpError: std::error::Error, Send, Sync, Display, Debug, ThreadAware);
#[test]
fn assert_size_small() {
assert_eq!(size_of::<HttpError>(), 64);
}
#[test]
fn validation_ok() {
let error = HttpError::validation("my-validation");
assert_eq!(error.message(), "my-validation");
assert_eq!(error.label(), "validation");
assert_eq!(error.recovery(), RecoveryInfo::never());
}
#[test]
fn invalid_status_code_ok() {
let error = HttpError::invalid_status_code(StatusCode::NOT_FOUND, RecoveryInfo::unknown());
assert_eq!(error.message(), "the response was not successful, status code: 404");
assert_eq!(error.label(), "invalid_status_code");
assert_eq!(error.recovery(), RecoveryInfo::unknown());
}
#[test]
fn other_method_wraps_custom_errors() {
let io_error = std::io::Error::other("custom error");
let error = HttpError::other(io_error, RecoveryInfo::retry(), "custom");
assert_eq!(error.message(), "custom error");
assert_eq!(error.label(), "custom");
assert_eq!(error.recovery(), RecoveryInfo::retry());
}
#[test]
fn http_constructor() {
let invalid_method = http::Method::from_bytes(b"INVALID METHOD").unwrap_err();
let error = HttpError::from(invalid_method);
assert_eq!(error.recovery(), RecoveryInfo::never());
assert_eq!(error.label(), "invalid_method");
}
#[test]
fn from_io() {
let error = HttpError::from(std::io::Error::other("test"));
assert_eq!(error.message(), "test");
assert_eq!(error.recovery(), RecoveryInfo::never());
assert_eq!(error.label(), "other");
let error = HttpError::from(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "some message"));
assert_eq!(error.recovery(), RecoveryInfo::retry());
assert_eq!(error.label(), "broken_pipe");
}
#[test]
fn from_uri_errors() {
let uri_error = "invalid uri with spaces".parse::<http::Uri>().unwrap_err();
let error = HttpError::from(uri_error);
assert_eq!(error.recovery(), RecoveryInfo::never());
assert_eq!(error.label(), "invalid_uri");
}
#[test]
fn assert_from() {
static_assertions::assert_impl_all!(HttpError: From<http::Error>);
static_assertions::assert_impl_all!(HttpError: From<InvalidUri>);
static_assertions::assert_impl_all!(HttpError: From<InvalidUriParts>);
static_assertions::assert_impl_all!(HttpError: From<InvalidHeaderValue>);
static_assertions::assert_impl_all!(HttpError: From<InvalidMethod>);
static_assertions::assert_impl_all!(HttpError: From<InvalidStatusCode>);
static_assertions::assert_impl_all!(HttpError: From<MaxSizeReached>);
static_assertions::assert_impl_all!(HttpError: From<templated_uri::ValidationError>);
static_assertions::assert_impl_all!(HttpError: From<std::io::Error>);
}
#[test]
fn assert_from_infallible() {
static_assertions::assert_impl_all!(HttpError: From<std::convert::Infallible>);
}
#[test]
fn timeout_error() {
let duration = Duration::from_millis(1500);
let timeout_error = HttpError::timeout(duration);
assert_eq!(timeout_error.recovery(), RecoveryInfo::retry());
assert_eq!(
timeout_error.message(),
"request timed out while receiving the response, timeout: 1500ms"
);
assert_eq!(timeout_error.label(), "timeout");
}
#[test]
fn timeout_for_body_error() {
let duration = Duration::from_millis(2500);
let error = HttpError::timeout_for_body(duration);
assert_eq!(error.recovery(), RecoveryInfo::retry());
assert_eq!(error.message(), "body data was not fully received, timeout: 2500ms");
assert_eq!(error.label(), "timeout");
}
#[test]
fn unavailable_error() {
let unavailable_error = HttpError::unavailable("service is down");
assert_eq!(unavailable_error.recovery(), RecoveryInfo::unavailable());
assert_eq!(unavailable_error.message(), "service is down");
assert_eq!(unavailable_error.label(), "unavailable");
}
#[test]
fn other_with_recovery() {
let existing_error = HttpError::validation("base error");
let error = HttpError::other_with_recovery(existing_error, "permission");
assert!(error.message().contains("base error"));
assert_eq!(error.label(), "permission");
assert_eq!(error.recovery().kind(), RecoveryKind::Never);
}
#[test]
fn rejected_request_ok() {
let request = HttpRequestBuilder::new_fake().uri("https://dummy").build().unwrap();
let mut error = HttpError::validation("rejection").with_request(request);
assert_eq!(error.take_request().unwrap().uri().to_string(), "https://dummy/");
assert!(error.take_request().is_none());
}
#[test]
fn relocated_preserves_error() {
let affinity = pinned_affinities(&[1])[0];
let error = HttpError::validation("relocated test");
let relocated = error.relocated(MemoryAffinity::Unknown, affinity);
assert_eq!(relocated.message(), "relocated test");
assert_eq!(relocated.label(), "validation");
}
}