httprustrust-axum

How to handle Json and Html in the same handler with Axum?


I would like to have a custom Response type with Axum and map the business logic to HTTP responses like this:

use axum::{
    http::StatusCode,
    response::{Html, IntoResponse},
    Json,
};
use serde::Serialize;

// Response

pub enum Response<T = ()> {
    Created,
    NoContent,
    JsonData(T),
    HtmlData(T),
}

impl<T> IntoResponse for Response<T>
where
    T: Serialize
{
    fn into_response(self) -> axum::response::Response {
        match self {
            Self::Created => StatusCode::CREATED.into_response(),
            Self::NoContent => StatusCode::OK.into_response(),
            Self::JsonData(data) => (StatusCode::OK, Json(data)).into_response(),
            Self::HtmlData(data) => (StatusCode::OK, Html(data)).into_response(),
        }
    }
}

However, this does not work.

no method named `into_response` found for tuple `(StatusCode, Html<T>)` in the current scope
method not found in `(StatusCode, Html<T>)

I was wondering why can't I just use Html the same way Json is used.


Solution

  • The problem is the different trait bounds on the IntoResponse implementations for Html and Json, while Json takes anything that implements Serialize:

    impl<T> IntoResponse for Json<T>
    where
        T: Serialize,
    

    For Html you must pass in something implementing Into<Body>:

    impl<T> IntoResponse for Html<T>
    where
        T: Into<Body>,
    

    axum simply doesn't know how to create html for arbitrary serializable data.

    You could simply add that trait bound to your impl IntoResponse:

    impl<T> IntoResponse for Response<T>
    where
        T: Serialize + Into<Body>
    

    It's likely you want to split up the type parameters to not enforce unnecessary trait bounds for the JsonData or HtmlData types.

    Another approach you could take is to wrap the data from the Html case in a wrapper that knows how to construct a body from arbitrary serializable data.

    struct SerializeBody<T>(pub T);
    impl<T: Serialize> From<SerializeBody<T>> for Body {
        fn from(SerializeBody(data): SerializeBody<T>) -> Body {
            todo!("left for the reader to decide how it should look")
        }
    }
    

    and wrapp the data in into_response:

    - Self::HtmlData(data) => (StatusCode::OK, Html(data)).into_response(),
    + Self::HtmlData(data) => (StatusCode::OK, Html(SerializeBody(data))).into_response(),