Skip to content

Error handling

In UserAgent::extract you're only handling the happy path: the method panics if the User-Agent header contains characters that are not ASCII printable. Panicking for bad user input is poor behavior: you should handle the issue gracefully and return an error instead.

Let's change the signature of UserAgent::extract to return a Result instead:

app/src/user_agent.rs
use pavex::http::header::{ToStrError, USER_AGENT};
use pavex::request::RequestHead;
// [...]
impl UserAgent {
    pub fn extract(request_head: &RequestHead) -> Result<Self, ToStrError /* (1)! */> {
        let Some(user_agent) = request_head.headers.get(USER_AGENT) else {
            return Ok(UserAgent::Unknown);
        };

        user_agent.to_str().map(|s| UserAgent::Known(s.into()))
    }
}
  1. ToStrError is the error type by HeaderValue's to_str when there is a non-printable ASCII character in the header.

All errors must be handled

If you try to build the project now, you'll get an error from Pavex:

ERROR: 
  × You registered a constructor that returns a `Result`, but you did not
   register an error handler for it. If I don't have an error handler, I
   don't know what to do with the error when the constructor fails!
  
       ╭─[app/src/blueprint.rs:12:1]
    12 │     configuration::register(&mut bp);
    13 │     bp.request_scoped(f!(crate::user_agent::UserAgent::extract));
       ·                       ────────────────────┬────────────────────
       ·                The fallible constructor was registered here
    14       ╰────
     help: Add an error handler via `.error_handler`

The invocation of `pavex [...] generate [...]` exited with a non-zero status code: 1
error: Failed to run `bp`, the code generator for package `server_sdk`

Pavex is complaining: you can register a fallible constructor, but you must also register an error handler for it.

Add an error handler

An error handler must convert a reference to the error type into a Response (1).
It decouples the detection of an error from its representation on the wire: a constructor doesn't need to know how the error will be represented in the response, it just needs to signal that something went wrong.
You can then change the representation of an error on the wire without touching the constructor: you only need to change the error handler.

  1. Error handlers, just like request handlers and constructors, can take advantage of dependency injection! You could, for example, change the response representation according to the Accept header specified in the request.

Define a new invalid_user_agent function in app/src/user_agent.rs:

app/src/user_agent.rs
// [...]
pub fn invalid_user_agent(_e: &ToStrError) -> Response {
    Response::bad_request()
        .set_typed_body("The `User-Agent` header value can only use ASCII printable characters.")
}

Then register the error handler with the Blueprint:

app/src/blueprint.rs
// [...]
pub fn blueprint() -> Blueprint {
    // [...]
    bp.request_scoped(f!(crate::user_agent::UserAgent::extract))
        .error_handler(f!(crate::user_agent::invalid_user_agent));
        // [...]
}

The application should compile successfully now.