Skip to main content

HttpError

Enum HttpError 

Source
#[repr(C)]
pub enum HttpError { Url(String), Io(String), Timeout,
#[non_exhaustive]
Http { code: u16, message: String, headers: Box<HeaderMap>, body: Vec<u8>, }, Json(String), BodyAlreadyTaken, InvalidStatusCode(u16), }
Expand description

An error produced when an HTTP request fails.

Variants fall into two groups:

Transport errors — generated by the shell when it cannot complete the HTTP exchange. These cross the FFI boundary and are serialized in the protocol: Url, Io, Timeout.

Processing errors — generated on the Rust side after a response arrives. These are never serialized or visible to shells:

  • Http — produced by Response::new() when the server returns a 4xx or 5xx status, and only then. At the protocol level these arrive as HttpResult::Ok; Response::new() converts them here, so app code using crux_http::Result<Response<T>> will see them as Err(HttpError::Http { code, .. })never as Ok(response) with an error status.
  • Json — produced when response body deserialisation fails.
  • BodyAlreadyTaken — a caller took the response body twice.
  • InvalidStatusCode — the shell sent a status that isn’t valid HTTP.

§Reading what the server said

Because a rejection arrives as an error rather than a response, everything the server sent with it lives on this type: HttpError::body and HttpError::body_json for the body, HttpError::header and HttpError::content_type for the headers. No matching on the variant’s fields required:

#[derive(Deserialize)]
struct ApiError {
    error: String,
}

// the `crux_http::Result` a feature receives when the server rejects a request
let result: crux_http::Result<crux_http::Response<Vec<u8>>> =
    crux_http::testing::rejection(409, r#"{"error":"that overlaps a booked day"}"#);

let error = result.expect_err("a 409 is never Ok");
assert_eq!(error.code(), Some(409));
assert_eq!(
    error.body_json::<ApiError>().unwrap().error,
    "that overlaps a booked day"
);

Variants§

§

Url(String)

The request URL could not be parsed.

§

Io(String)

An IO error prevented the request from completing.

§

Timeout

The request timed out before a response was received.

§

#[non_exhaustive]
Http

The exchange completed, but the server answered with a 4xx or 5xx status.

This is what a feature receives instead of a Response when the server rejects a request, so it is the only place a rejection can be handled — and a rejection is the only thing that produces it. Nothing else in the crate raises an Http, so matches!(error, HttpError::Http { .. }) (or, more simply, code returning Some) means the server said no.

body is the response body exactly as the server sent it. Body decoding (expect_json, expect_string) is skipped for an error status, so a server’s error envelope — {"error": "…"}, an RFC 7807 problem+json document, a plain sentence — survives here even for a request built with expect_json. Read it with HttpError::body or HttpError::body_json rather than destructuring.

code is the status code and message its canonical reason phrase (e.g. "409 Conflict") — a description of the status, never the server’s own explanation. Display shows only those two, so an app that reports error.to_string() to a user shows "HTTP error 409: 409 Conflict" and discards whatever the server took the trouble to say.

headers are the rejected response’s headers, because a rejection’s policy often lives there and nowhere else: Retry-After on a 429 or 503, WWW-Authenticate on a 401, rate-limit headers, or the Content-Type that says whether the body is JSON, HTML or prose. Read them with HttpError::header, HttpError::content_type or HttpError::headers. A rejection that sent no headers has an empty map, not an absent one — there was always a response here, which is what distinguishes this variant from every other.

(The map is boxed only to keep HttpError small: a bare HeaderMap is 96 bytes, and this type is the Err of nearly every function in the crate.)

The variant is #[non_exhaustive]: only crux_http constructs it, so that what a rejection carries can grow without breaking you. Match it with { code, .. } and read the rest through the accessors above; in tests, build one with rejection or rejection_from.

Fields

This variant is marked as non-exhaustive
Non-exhaustive enum variants could have additional fields added in future. Therefore, non-exhaustive enum variants cannot be constructed in external crates and cannot be matched against.
§code: u16
§message: String
§headers: Box<HeaderMap>
§body: Vec<u8>
§

Json(String)

A response body could not be deserialized (or a request body serialized).

§

BodyAlreadyTaken

The response body had already been taken.

A caller error rather than anything the server did: body_bytes, body_string and body_json all take the body, so only the first call can succeed. It carries nothing, because the caller still holds the Response and so already has its status and headers.

§

InvalidStatusCode(u16)

The shell sent a status code that is not valid HTTP (outside the range 100–999).

The response never became one, so there is nothing to read from it. This means the shell is misbehaving, not that the request was rejected.

Implementations§

Source§

impl HttpError

Source

pub const fn code(&self) -> Option<u16>

The status the server rejected the request with.

Some if and only if this error is a rejection — nothing else in the crate carries a status. None covers everything the server didn’t decide: transport failures, a body that wouldn’t deserialize, a body taken twice, and a status the shell sent that wasn’t valid HTTP (that one keeps its number on InvalidStatusCode, which is not a status a server ever answered with).

§Examples
let error: HttpError = crux_http::testing::rejection::<Vec<u8>>(404, "")
    .expect_err("a 404 is never Ok");
assert_eq!(error.code(), Some(404));

assert_eq!(HttpError::Timeout.code(), None);
assert_eq!(HttpError::InvalidStatusCode(999).code(), None);
Source

pub fn body(&self) -> Option<&[u8]>

The raw response body that came with an error status, if there was one.

This is the server’s own account of why it rejected the request — usually far more useful to a user than Display, which can only report the status. None for anything that isn’t a rejection, and for a rejection whose body was empty — every real 4xx arrives with bytes, and Some(&[]) would only make the obvious if let Some(body) idiom display blanks.

§Examples
let error = crux_http::testing::rejection::<Vec<u8>>(422, "name is required")
    .expect_err("a 422 is never Ok");
assert_eq!(error.body(), Some(&b"name is required"[..]));
Source

pub fn header(&self, name: impl AsHeaderName) -> Option<&HeaderValue>

A header from the rejected response.

Some of what an app needs to act on a rejection is only in the headers: Retry-After on a 429 or 503, WWW-Authenticate on a 401, rate-limit headers. None of it can be recovered from the status or the body.

Returns None for anything that isn’t a rejection, and for a name the rejected response didn’t carry. Lookup is case-insensitive, as HTTP requires.

§Examples
let error = rejection_from::<Vec<u8>>(
    HttpResponse::status(429).header("retry-after", "30").build(),
)
.expect_err("a 429 is never Ok");

assert_eq!(error.header("Retry-After").unwrap(), "30");
Source

pub fn headers(&self) -> Option<&HeaderMap>

All headers from the rejected response.

The escape hatch for anything HttpError::header and HttpError::content_type don’t cover — multi-value headers, iteration, or forwarding the lot to a logger.

Some for a rejection — possibly an empty map, if the server sent no headers — and None for every other error, none of which had a response behind them.

Source

pub fn content_type(&self) -> Option<Mime>

The content type of the error response body, if it declared one.

Tells an error envelope (application/json) from an RFC 7807 document (application/problem+json) from a proxy’s HTML page (text/html) — which decides whether it is worth handing the body to HttpError::body_json, and whether the body is safe to show a user as text.

§Examples
let error = rejection_from::<Vec<u8>>(
    HttpResponse::status(400)
        .header("content-type", "application/problem+json")
        .body(br#"{"detail":"nope"}"#.to_vec())
        .build(),
)
.expect_err("a 400 is never Ok");

assert_eq!(
    error.content_type().map(|mime| mime.to_string()),
    Some("application/problem+json".to_string())
);
Source

pub fn body_json<T: DeserializeOwned>(&self) -> Result<T>

Deserialize the error response body from JSON.

Works for any JSON error shape — deserialize into your API’s own envelope, or into an RFC 7807 problem+json struct, or into serde_json::Value if you don’t know which you’ll get.

§Errors

Returns HttpError::Json if the error carries no body, or if the body is not valid JSON for T.

§Examples
#[derive(Deserialize)]
struct Problem {
    detail: String,
}

let error = crux_http::testing::rejection::<Vec<u8>>(
    409,
    r#"{"title":"Conflict","detail":"that would create a management cycle"}"#,
)
.expect_err("a 409 is never Ok");

let problem: Problem = error.body_json().unwrap();
assert_eq!(problem.detail, "that would create a management cycle");

Trait Implementations§

Source§

impl Clone for HttpError

Source§

fn clone(&self) -> HttpError

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for HttpError

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for HttpError

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Display for HttpError

Source§

fn fmt(&self, __formatter: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Error for HttpError

1.30.0 · Source§

fn source(&self) -> Option<&(dyn Error + 'static)>

Returns the lower-level source of this error, if any. Read more
1.0.0 · Source§

fn description(&self) -> &str

👎Deprecated since 1.42.0:

use the Display impl or to_string()

1.0.0 · Source§

fn cause(&self) -> Option<&dyn Error>

👎Deprecated since 1.33.0:

replaced by Error::source, which can support downcasting

Source§

fn provide<'a>(&'a self, request: &mut Request<'a>)

🔬This is a nightly-only experimental API. (error_generic_member_access)
Provides type-based access to context intended for error reports. Read more
Source§

impl<'ʄ> Facet<'ʄ> for HttpError

Source§

const SHAPE: &'static Shape

The shape of this type, including: whether it’s a Struct, an Enum, something else? Read more
Source§

impl From<Error> for HttpError

Source§

fn from(e: Error) -> Self

Converts to this type from the input type.
Source§

impl From<Error> for HttpError

Source§

fn from(e: Error) -> Self

Converts to this type from the input type.
Source§

impl From<Error> for HttpError

Source§

fn from(e: Error) -> Self

Converts to this type from the input type.
Source§

impl From<ParseError> for HttpError

Source§

fn from(e: ParseError) -> Self

Converts to this type from the input type.
Source§

impl PartialEq for HttpError

Source§

fn eq(&self, other: &HttpError) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Serialize for HttpError

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl Eq for HttpError

Source§

impl StructuralPartialEq for HttpError

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,