Need help with askama_axum error handling

I have gotten my code to work, but with less than optimal error handling.

Here is my Template struct:

#[derive(Template)]
#[template(path = "bookmarks.html")]
pub struct ShowBookmark<'a> {
   pub bookmarks: &'a Vec<Bookmark>
}
...
#[derive(Debug, Deserialize, Serialize, FromRow)]
pub struct Bookmark {
    #[sqlx(rename = "title")]
    pub description: String,
    pub href: String,
    pub tags: String,
    pub extended: String,
    #[sqlx(rename = "public")]
    pub shared: String,
    pub toread: String,
    #[sqlx(rename = "created_at")]
    pub time: chrono::DateTime<chrono::Utc>,
}

This handler works:

pub async fn read(
    extract::State(pool): extract::State<PgPool>,
) -> Html<String> {
    // Beware of the size of the result set! Consider using `LIMIT`
    let res = sqlx::query_as::<_, Bookmark>("SELECT * FROM bookmarks")
        .fetch_all(&pool)
        .await;

    let html = ShowBookmark { bookmarks: &res.unwrap() };
    Html(html.render().unwrap())
}

As a newbie, I can see that there are, I think, 3 places my handler can panic:

  • At res (database error)
  • When I &res.unwrap() to provide value for html.bookmarks
  • At html.render().unwrap()

I have tried writing the function -> Result<Html<String>, askama_axum::Error> and returning OK(Html(html.render()?)), but compiler tells me that Error does not implement IntoResponse trait. If I try
extract::reject::UnknownBodyError instead of askama_axum::Error

I get a compiler error that tells me:

the trait std::convert::From<askama_axum::Error> is not implemented for axum::extract::rejection::UnknownBodyError.

There is other information in the diagnostic message but I do not understand most of it.

It seems to me that using askama_axum::Error would be the most straightforward, but I need to understand how to implement IntoResponse trait for this error type.

the orphan rule does not allow you to implement a foreign trait on a foreign type. What is usually done in these cases is to create your own domain error type, and/or use an error handling crate such as anyhow.

2 Likes

Thank you. I decided to make my own error type, modeling it on the askama_axum docs section for askama_axum::IntoResponse.

use axum::{extract, http, response::{Html, IntoResponse, Response}};

#[derive(Debug)]
pub enum ReadError {
    DatabaseError,
    TemplateError,
}

impl IntoResponse for ReadError {
    fn into_response(self) -> Response {
        let body = match self {
            ReadError::DatabaseError => "PgDatabaseError",
            ReadError::TemplateError => "Error rendering template",
        };
        (http::StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
    }
}

#[debug_handler]
pub async fn read(
    extract::State(pool): extract::State<PgPool>,
) -> Result<Html<String>, ReadError> {
    // Beware of the size of the result set! Consider using `LIMIT`
    let res: Result<Vec<Bookmark>, crate::handlers::ReadError> = match sqlx::query_as("SELECT * FROM bookmarks")
        .fetch_all(&pool)
        .await {
            Ok(res) => Ok(res),
            Err(_e) => Err(ReadError::DatabaseError),
        };

    let html = ShowBookmark { bookmarks: &res? };
    let page: Result<String, crate::handlers::ReadError> = match html.render() {
        Ok(page) => if !html.bookmarks.is_empty() { Ok(page) } else { Err(ReadError::TemplateError) },
        Err(_e) => Err(ReadError::TemplateError),
    };
    Ok(Html(page?))
}

I can test this by querying SELECT * FROM *[incorrect table name]* in my handler code. The handler no longer panics, instead successfully rendering a page with plain text "PgDatabaseError" (as per the into_response method in my new code).

Testing for a ReadError::TemplateError was more difficult since mistakes in my Template struct or my bookmarks.html template trigger compilation errors. I tested this my querying SELECT * FROM bookmarks WHERE id = *[row that does not exitst]* which to my surprise does not raise a DatabaseError but instead delivers ShowBookmarks { bookmarks: [] } and my code rendered an empty page. That is why I added if !html.bookmarks.is_empty()... . I was able to force a TemplateError that rendered text "Error rendering template" as expected.

I wouldn't be surprised if there is more correct way of doing this. Am I on the right track? Your help is very much appreciated.

Yes, you are on the right track. In your code, you used pattern matching for transforming the error case, but there are better ways to do this; to name a few:

  • It's more idiomatic to use the try operator (?) to "bubble up" the error. If you implement the From trait on your ReadError type for the different third party error types that your server might face, you can use the ? operator and it will automatically transform the error using the From trait implementation.
  • You can use the map_err method from Result
  • As I mentioned before, you can use a crate such as anyhow to do this for you
1 Like

I decided to give anyhow a try. I read some basic intro tutorials and the anyhow docs. I thought, "This is going to be great." However, I quickly ran into this compilation error (from the diagnostic message):
the trait askama_axum::IntoResponse is not implemented for anyhow::Error

To my newbie brain, this sounds like where I started, butting up against the "orphan rule." I searched the repository issues for anyhow and for askama and did not find anything that helps. Maybe anyhow will not work with askama?

My guess is IntoResponse is only implemented for implementations of std's Error, which anyhow::Error doesn't (instead it implements From both ways so that ? just works)

Your options are too explicitly convert the error to a boxed std Error: result.map_err(Box::<dyn std::error::Error + Send + 'static>::from) should work, though it's unholy enough to need a wrapper; or you could use your own error type easily using thiserror (you can use both in your crate, actually, since thiserror is a library to derive std Error nicely, and anyhow is a library to erase the types of std Error)

Thanks. My first naive thought was to impl From for a new error struct CustomError so that I could convert anyhow::Error to my CustomError and then implement IntoResponse on my CustomError. However, I discovered through compile errors that my code did not, in fact, raise anyhow::Error's but rather sqlx::Error and askama_axum::Error. So I tinkered around a bit, without using anyhow and got this to work:

pub struct CustomError;

impl From<sqlx::Error> for CustomError {
    fn from(_e: sqlx::Error) -> CustomError {
        CustomError
    }
}

impl From<askama_axum::Error> for CustomError {
    fn from(_e: askama_axum::Error) -> CustomError {
        CustomError
    }
}

impl IntoResponse for CustomError {
    fn into_response(self) -> Response {
        let body = "Converted Error to CustomError";
        (http::StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
    }
}

pub async fn read(
    extract::State(pool): extract::State<PgPool>,
) -> Result<Html<String>, CustomError> {
    // Beware of the size of the result set! Consider using `LIMIT`
    let res = sqlx::query_as::<_, Bookmark>("SELECT * FROM bookmarks")
        .fetch_all(&pool)
        .await;

    let html = ShowBookmark { bookmarks: &res? };
    Ok(Html(html.render()?))
}

However, it makes no sense to have to write separate From implementations for each type of error. I just cannot get it to work with generics.

I will take a look at thiserror.

Yeah, thiserror can also implement all the From implementations for you too, a simple version:

#[derive(Debug, thiserror::Error)]
pub enum CustomError {
  #[error("database error: {0}")]
  Sqlx(#[from] sqlx::Error),
  #[error("rendering error: {0}")]
  Asxama(#[from] askama_axum::Error)]
}

There's a lot more details, but mostly it can do whatever you are likely to want to with manual error implementations, just much more easily.

Yes. Thanks. I was working on that just now. I think I will figure this out.

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.