Best way to return transformed error

I'm writing a response handler for Axum, and I have a function that returns a Result<DraftTicket, Box<dyn Err>>.

Here's what I currently have:

async fn create_ticket(extract::Json(draft): Json<TicketDraftRequest>, state: State<StateType>) -> Response {
    // do our conversion check first
    let draft = TicketDraft::try_from(draft);
    if let Err(e) = draft {
        return (StatusCode::BAD_REQUEST, e.to_string()).into_response();
    }
    let draft = draft.unwrap(); // we know this is OK
    // rest of function

But that last unwrap call seems off to me, and I'm wondering if there's a better way.

Putting it in a match statement seems non-ideal since I only want to return on error (and match would force me to nest the remaining part of the function).

I was wondering if there is some construct like this where I could transform the error into a Response and return that if error:

async fn create_ticket(extract::Json(draft): Json<TicketDraftRequest>, state: State<StateType>) -> Response {
    // do our conversion check first
    let draft = TicketDraft::try_from(draft).map_err(|e| {
        (StatusCode::BAD_REQUEST, e.to_string()).into_response()
    })?;

But the question mark operator doesn't work here (because the function returns a Response), so, is there a more rust-preferred solution to this type of flow, where you want to return a transformed error (on error) and otherwise continue? Or is what I did (just using unwrap) the correct solution?

match is not as concise as using Result combinators but it is very clear and in this case I think the best (only?) way, without ugly unwrapping. You don't have to nest, since the match expression can return the draft variable.

        let draft = match draft {
            Err(e) => return (StatusCode::BAD_REQUEST, e.to_string()).into_response();
            Ok(draft) => draft,
        };
2 Likes

I don't know about "best," and maybe there isn't one, but I do have a solution I'm content with.

First, I have an Internal type that I use to log "unhandled" errors. To minimize the amount of info error messages disclose to clients, the error message is not sent to the client, but you could simplify if you aren't worried about that:

type BoxedError = Box<dyn error::Error + Send + Sync>;

// Returns a 500 Internal Server Error to the client. Meant to be used via the
// `?` operator; _does not_ return the originating error to the client.
#[derive(Debug)]
pub struct Internal(Id, BoxedError);

impl<E> From<E> for Internal
where
    E: Into<BoxedError>,
{
    fn from(error: E) -> Self {
        let id = Id::generate();
        Self(id, error.into())
    }
}

impl IntoResponse for Internal {
    fn into_response(self) -> Response {
        let Self(id, error) = self;
        eprintln!("hi: [{id}] {error}");
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("internal server error\nerror id: {id}"),
        )
            .into_response()
    }
}

Second, endpoints like your create_ticket function return a result over Internal:

// 
async fn create_ticket(extract::Json(draft): Json<TicketDraftRequest>, state: State<StateType>) -> Result<CreateTicketResponse, Internal> {
    // do our conversion check first
    let draft = TicketDraft::try_from(draft)?; // <-- replaces your unwrap()
    // … rest of handler …
}

In situations where the error response needs to be more complicated, I mix this with a per-handler error type. Here's an example from my own code:

async fn events(
    State(app): State<App>,
    identity: Identity,
    last_event_id: Option<LastEventId<Sequence>>,
    Query(query): Query<EventsQuery>,
) -> Result<Events<impl Stream<Item = types::Event> + std::fmt::Debug>, EventsError> {
    let resume_at = last_event_id
        .map(LastEventId::into_inner)
        .or(query.resume_point);

    let stream = app.events().subscribe(resume_at).await?;
    let stream = app.tokens().limit_stream(identity.token, stream).await?;

    Ok(Events(stream))
}

// … 

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum EventsError {
    DatabaseError(#[from] sqlx::Error),
    ValidateError(#[from] ValidateError),
}

impl IntoResponse for EventsError {
    fn into_response(self) -> Response {
        match self {
            Self::ValidateError(ValidateError::InvalidToken) => Unauthorized.into_response(),
            other => Internal::from(other).into_response(),
        }
    }
}
1 Like

Double-responding because I had a second idea.

You might be able to get Axum to do the dirty work for you in this specific case, by having your method accept a Json<TicketDraft> instead of a Json<Draft> argument. TicketDraft can be annotated with #[serde(try_from = "Draft")], and if the conversion fails, then Axum will return a 422 Unprocessable Content response, instead of calling your endpoint.

3 Likes

This works beautifully - and results in really clean code. Thank you!

Also the explanation of building a custom error for response also seems interesting. I think it might be overkill for this project, but I really appreciate the reference.

Also, @jumpnbrownweasel - I completely forgot you could use a match with an assignment vs just using it as flow control :sweat_smile: - that is much cleaner than my if/let/unwrap!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.