#[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 byResponse::new()when the server returns a 4xx or 5xx status, and only then. At the protocol level these arrive asHttpResult::Ok;Response::new()converts them here, so app code usingcrux_http::Result<Response<T>>will see them asErr(HttpError::Http { code, .. })— never asOk(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
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
impl HttpError
Sourcepub const fn code(&self) -> Option<u16>
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);Sourcepub fn body(&self) -> Option<&[u8]>
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"[..]));Sourcepub fn header(&self, name: impl AsHeaderName) -> Option<&HeaderValue>
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");Sourcepub fn headers(&self) -> Option<&HeaderMap>
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.
Sourcepub fn content_type(&self) -> Option<Mime>
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())
);Sourcepub fn body_json<T: DeserializeOwned>(&self) -> Result<T>
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<'de> Deserialize<'de> for HttpError
impl<'de> Deserialize<'de> for HttpError
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl Error for HttpError
impl Error for HttpError
1.30.0 · Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()