Skip to main content

Module error_handling

Module error_handling 

Source
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::Error for custom extractors
  • The Err variant of a Result return 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
        }
    }
}