The Rocket framework framework
required endpoint to return values that implement responder. The works
similarly for Result<T, E>
. Both T
and
E
need to implement responder. But what if you want to use
a library like anyhow or
eyre to simplify error
handling while keeping that idiomatic rust? Using some rust magic this
is possible.
?
operatorFor more readable error handling, rust provides the ?
operator, which can terminate a function early by aborting in case of an
error when the function returns a result. Lets take the following
code
#[derive(Debug)]
enum MathError {
,
DivisionByZero,
NonPositiveLogarithm,
NegativeSquareRoot}
fn div(x: f64, y: f64) -> Result<f64, MathError> {
if y == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(x / y)
}
}
fn div_twice(x: f64, y: f64) -> Result<f64, MathError> {
let once = div(x, y)?;
, y)
div(once}
If a zero would be passed to the y
parameter in the
div_twice
function, then the second division would never
start in the first place. Under the hood some extra magic happens as
well.
If needed, into
is called the error, to convert it into
the required type. With this in mind, lets start building our error. For
this example in adhering to RFC 7807.
First lets define a error type. This type be passed around our
application. For this example, I’m using the eyre
crate,
but this would work with the Error
type in the
anyhow
crate as well.
#[derive(Debug)]
pub(crate) struct Error {
: eyre::Report,
inner: Option<String>,
error_type: Option<Status>,
status: Option<String>,
title: Option<String>,
detail: Option<String>,
instance}
impl Error {
pub(crate) fn new(inner: impl Into<eyre::Report>) -> Self {
Self {
: inner.into(),
inner: None,
status: None,
error_type: None,
title: None,
detail: None,
instance}
}
pub(crate) fn with_status(self, status: Status) -> Self {
let mut new = self;
.status = Some(status);
new
new}
}
impl<E: Into<eyre::Report>> From<E> for Error {
fn from(inner: E) -> Self {
Self::new(inner)
}
}
Our error type has the inner
field, which points to the
error itself. Here we could get the backtrace of other information is
needed. The remaining fields is just metadata passed into the
response.
A simple constructor is added along with the into method. The into
method is quite important here. It allows us to accept any
std::error::Error
to be cast into our custom error.
Next a Result
type:
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
Lastly for our error related code, the code responding to in our endpoints.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
struct ErrorResponse {
#[serde(rename = "type")]
: String,
error_type: u16,
status: String,
title: Option<String>,
detail: String,
instance}
impl<'r, 'o: 'r> Responder<'r, 'o> for Error {
fn respond_to(self, request: &Request<'_>) -> response::Result<'o> {
let status = self.status.unwrap_or(Status::InternalServerError);
let error_response = ErrorResponse {
: self.error_type.unwrap_or_else(|| "about:blank".to_string()),
error_type: status.code,
status: self.title.unwrap_or_else(|| "An unknown error occured".to_string()),
title: self.detail,
detail: self.instance.unwrap_or_else(|| request.uri().to_string()),
instance};
let error_string = serde_json::to_string(&error_response).unwrap();
Response::build()
.header(Header::new("Content-Type", "application/json+problem"))
.status(status)
.sized_body(error_string.len(), Cursor::new(error_string))
.ok()
}
}
First we define ErrorResponse
, which is a simple object
containing information about what went wrong. It almost directly maps to
our Error
.
Based on our error we may be able to provide all needed data to the response itself. If that’s not the case, we can have some sane defaults.
After defining all these types, we can use it in a rocket application as so:
#[rocket::get("/error")]
fn error_route() -> Error {
io::Error::new(ErrorKind::Other, "Something went wrong").into()
}
In this route, we always return an error with the default response.
To add a status code to the response, we can use the
with_status
method defined earlier.
#[rocket::get("/unauthorized")]
fn unauthorized_route() -> Error {
io::Error::new(ErrorKind::Unauthorized, "You are not allowed to do this")
.into()
.with_status(Status::Unauthorized)
}
Lastly, for fallible route, we can return a result or abort early
with the ?
operator.
#[rocket::get("/fallible")]
fn fallible_route() -> Result<String> {
let result = some_fallible_function()?;
Ok(result)
}
If we need to pass a status to the error. We could use the
map_err
method.
#[rocket::get("/fallible")]
fn fallible_route() -> Result<String> {
let result = some_fallible_function()
.map_err(|e| e.into().with_status(Status::BadRequest))?;
Ok(result)
}
© 2024 Nils de Groot