Expand description
§Error handling
Errors in trillium-api are handlers. Whether an extraction fails or
your handler returns a Result::Err, the error value is run on the conn
just like any other handler. This means error responses are fully
customizable with the same tools you use for success responses.
§Extraction errors
When a TryFromConn extractor fails, its
Error type is run as a handler on the conn instead of your handler
function. The simplest error type is Status:
use trillium::{Conn, Status};
use trillium_api::TryFromConn;
use trillium_router::RouterConnExt;
struct UserId(u64);
impl TryFromConn for UserId {
type Error = Status;
async fn try_from_conn(conn: &mut Conn) -> Result<Self, Status> {
conn.param("user_id")
.and_then(|p| p.parse().ok())
.map(UserId)
.ok_or(Status::BadRequest) // sets 400, no body
}
}§Built-in Error type
Body<T> and Json<T> use
Error as their extraction error type. This type
implements Handler with a before_send hook that serializes itself
as a JSON error response with an appropriate status code:
- Parse errors →
422 Unprocessable Entity - Missing content type →
415 Unsupported Media Type - Unsupported content type →
415 Unsupported Media Type - I/O errors →
400 Bad Request
use trillium_api::{api, Body};
use trillium::Conn;
#[derive(serde::Deserialize)]
struct Input { name: String }
async fn handler(_conn: &mut Conn, Body(input): Body<Input>) -> String {
format!("hello, {}", input.name)
}
// Sending invalid JSON returns a structured error response:
// Response body: {"error":{"type":"parse_error","path":".","message":"..."}}§Result return types
When your handler returns Result<T, E> where both T and E
implement Handler, the result itself is a handler:
use trillium::{Conn, Handler, Status};
use trillium_api::{api, Json};
#[derive(serde::Serialize)]
struct ApiError { message: String }
/// Implement Handler on your error type to control the error response.
impl Handler for ApiError {
async fn run(&self, conn: Conn) -> Conn {
conn.with_json(self)
.with_status(Status::BadRequest)
.halt()
}
}
async fn create(_conn: &mut Conn, _: ()) -> Result<Json<String>, ApiError> {
if true {
Ok(Json("created".into()))
} else {
Err(ApiError { message: "something went wrong".into() })
}
}§Custom error types
For real applications, you’ll typically define an error enum that
covers all your failure modes. The key requirement is that it
implements Handler:
use trillium::{Conn, Handler, Status};
use trillium_api::ApiConnExt;
#[derive(Debug, serde::Serialize, Clone)]
#[serde(tag = "error")]
enum AppError {
#[serde(rename = "not_found")]
NotFound { message: String },
#[serde(rename = "forbidden")]
Forbidden,
#[serde(rename = "internal")]
Internal { message: String },
}
impl Handler for AppError {
async fn run(&self, conn: Conn) -> Conn {
let status = match self {
AppError::NotFound { .. } => Status::NotFound,
AppError::Forbidden => Status::Forbidden,
AppError::Internal { .. } => Status::InternalServerError,
};
conn.with_json(self).with_status(status).halt()
}
}You can use this error type as:
- A
TryFromConn::Errorfor custom extractors - The
Errvariant of aResultreturn type
impl TryFromConn for Todo {
type Error = AppError;
async fn try_from_conn(conn: &mut Conn) -> Result<Self, AppError> {
// ...
}
}
async fn update(
_conn: &mut Conn,
(todo, Body(input)): (Todo, Body<UpdateTodo>),
) -> Result<Json<Todo>, AppError> {
// ...
}§Accessing Error from FromConn
Error itself implements FromConn, extracting (and
removing) any error that a previous handler placed into conn state.
This is useful for custom error formatting in a before_send handler:
impl Handler for CustomErrorHandler {
async fn run(&self, conn: Conn) -> Conn { conn }
async fn before_send(&self, mut conn: Conn) -> Conn {
if let Some(error) = conn.take_state::<AppError>() {
// format the error however you like
conn.with_json(&error).with_status(Status::BadRequest)
} else {
conn
}
}
}