Error handlers
Error handlers translate errors into HTTP responses, decoupling what went wrong from the way it's communicated to the caller.
use pavex::Response;
use pavex::methods;
#[derive(Debug, Clone, thiserror::Error)]
pub enum BearerExtractionError {
#[error("The request didn't set the `Authorization` header")]
MissingAuthorizationHeader,
#[error("The `Authorization` header is malformed")]
MalformedHeader,
}
// [...]
#[methods]
impl BearerExtractionError {
#[error_handler]
pub fn to_response(&self) -> Response {
use BearerExtractionError::*;
match self {
MissingAuthorizationHeader => {
Response::unauthorized()
.set_typed_body("Missing `Authorization` header")
}
MalformedHeader => {
Response::bad_request()
.set_typed_body("Malformed `Authorization` header")
}
}
}
}
Signature
Pavex accepts a wide range of function signatures for error handlers, as long as they satisfy these requirements:
- One input parameter is a reference1 (
&
) to the error type. - The return type implements the
IntoResponse
trait. - The return type isn't a
Result
2.
Other than that, you have a lot of freedom in how you define your error handlers:
- They can be free functions or methods.
- They can be synchronous or asynchronous.
- They can take one or more input parameters, leaning on Pavex's dependency injection system.
The next sections will elaborate on each of these points.
Defining error handlers
Use the #[error_handler]
attribute to define a new error handler:
use pavex::{http::StatusCode, methods};
#[derive(Debug)]
pub enum LoginError {
InvalidCredentials,
DatabaseError,
}
#[methods]
impl LoginError {
#[error_handler]
pub fn to_response(&self) -> StatusCode {
match self {
LoginError::InvalidCredentials => StatusCode::UNAUTHORIZED,
LoginError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
The signature of this error handler satisfies all the requirements listed in the previous section:
- It takes as input
&self
, a reference to the error type (LoginError
). - It returns a
StatusCode
, which implements theIntoResponse
trait. - It is infallible, i.e. it doesn't return a
Result
.
Registration
Use an import to register in bulk all the error handlers defined in the current crate:
use pavex::{Blueprint, blueprint::from};
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.import(from![crate]);
// [...]
}
Alternatively, register error handlers one by one using Blueprint::error_handler
:
use super::LOGIN_ERROR_TO_RESPONSE;
use pavex::Blueprint;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.error_handler(LOGIN_ERROR_TO_RESPONSE);
// [...]
}
Fallback error handler
The error handler from the previous section is specific: it takes as input a reference to the exact error type returned by the fallible component.
Specific error handlers are a great choice when you need the information encoded in the error type to customize the response returned to the caller—e.g. choose the most appropriate status code for each enum variant.
Nonetheless, you aren't required to register a specific error handler for every error type in your application.
If no specific error handler is registered, Pavex will invoke the fallback error handler—i.e. the error handler for pavex::Error
.
Customise the fallback
By default, Pavex will invoke pavex::Error::to_response()
as the fallback. Register an alternative error handler for pavex::Error
if you want to customize the fallback error logic:
use pavex::{
Response, error_handler,
http::{HeaderValue, header::LOCATION},
};
#[error_handler]
/// If there is no specific error handler for the given error,
/// redirect to a generic error page.
pub fn redirect_to_error_page(_e: &pavex::Error) -> Response {
let destination = HeaderValue::from_static("error");
Response::temporary_redirect().insert_header(LOCATION, destination)
}
Prefer opaque responses
The fallback error handler should return opaque responses—e.g. a 500 Internal Server Error
or a redirect to a generic error page. Don't include error details in the response: you're handling arbitrary errors in the fallback error handler, you may leak implementation details (or sensitive information!) to the caller.
Dependency injection
Error handlers can take advantage of dependency injection.
You specify the dependencies of your error handler as input parameters in its function signature. Those inputs are going to be built and injected by the framework, using the constructors you have registered.
use pavex::Response;
use pavex::methods;
// [...]
#[methods]
impl AuthError {
#[error_handler]
pub fn to_response(
#[px(error_ref)] &self,
organization_id: OrganizationId,
) -> Response {
// [...]
}
}
You must annotate the error reference with #[px(error_ref)]
, an helper attribute, if your error handler takes multiple input parameters.
This isn't necessary for error handlers with a single input parameter, since there is no ambiguity.
Check out the dependency injection guide for more details on how the process works.
-
All errors handled by Pavex are forwarded to your error observers—e.g. for logging purposes.
The borrow-checker wouldn't let us invoke your error observers if error handlers had previously consumed the error type by value. ↩ -
Error handlers perform a conversion. The error type should contain all the information required to build the HTTP response. If that's not the case, rework the fallible component to add the missing details to the error type, so that the error handler can be infallible. ↩