Axum: Return tuple with HTTP StatusCode and optional value: Option<T>

I am writing an Axum REST API and am curious how I can return an optional struct. I'm using the tuple return value, which allows me to also return an HTTP StatusCode enum value.

When I write an axum route handler that looks like the following example, I get a compiler error.

async fn item_post() -> (StatusCode, Option<Json<MyStruct>>) {
}

the trait bound fn(Option<Json<MyStruct>>) -> ... {mystruct_post}: Handler<_, _> is not satisfied
Consider using #[axum::debug_handler] to improve the error message
the following other types implement trait Handler<T, S>:
Layered<L, H, T, S> implements Handler<T, S>
MethodRouter<S> implements Handler<(), S>

I also tried this:

async fn item_post() -> (StatusCode, impl IntoResponse) {
}

But this resulted in:

impl Trait is not allowed in bounds
impl Trait is only allowed in arguments and return types of functions and methods
cannot define inherent impl for foreign type

Apparently you can only use impl IntoResponse as the top-level return type, not inside a tuple.

:light_bulb: Question: Is there a proper way to return an optional struct from an axum route handler?

What HTTP response do you want (some_status, None) to correspond to?

Can you share the definition of MyStruct, including any #[derive]d traits? What traits have you implemented on MyStruct yourself?

I figured the struct definition wasn't too important. I was just testing with a type like this:

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Vehicle {
    manufacturer: String,
    model: String,
    year: u32,
    id: Option<String>,
}

I have not added any impl blocks to the struct myself. Is implementing IntoResponse the correct course of action for this scenario?

I'm not quite sure what you mean by your initial question? I was wanting to throw an Internal Server Error, but also return None ... but if the API call succeeded, then return an instance of the Vehicle. Maybe I am not using the right pattern; I was just thinking there should be a way to optionally return a value or not?

Not necessarily, I just wanted to understand your program a bit better. That is where I've ended up in my own code, but that's due to factors that might not translate to yours.

I'm not quite sure what you mean by your initial question?

The point of an Axum handler method's return value is to generate an HTTP response, including its status code, headers, and body. The rust representation of the response is your application's means to that single end. If you are returning (status, None) from a handler method, what HTTP response do you expect your service to send to the client?

The reason I ask is that Axum can only route to functions that return IntoResponse types. Axum has a fairly large collection of implementations, including an implementation of IntoResponse for tuple types of the form (T1, T2), so long as T1 and T2 both implement IntoResponse themselves so long as T2 implements IntoResponse and T1 implements IntoResponseParts. However, there is no implementation of IntoResponse for Option.

In order for your program to work, you'll need to decide how you want your Option<Json<Vehicle>> to be represented as an HTTP response, and then implement that. For coherence reasons, you'll likely have to wrap the whole mess in a new type, and at that point it might be easier to define your own option-like response type, instead.

Putting that all together gives this:

use axum::{
    Json,
    http::StatusCode,
    response::{IntoResponse, Response},
};

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Vehicle {
    manufacturer: String,
    model: String,
    year: u32,
    id: Option<String>,
}

async fn item_get() -> (StatusCode, JsonOption<Vehicle>) {
    (StatusCode::OK, JsonOption::None)
}

enum JsonOption<T> {
    Some(T),
    None,
}

impl<T> IntoResponse for JsonOption<T>
where
    Json<T>: IntoResponse,
{
    fn into_response(self) -> Response {
        match self {
            Self::Some(val) => Json(val).into_response(),
            Self::None => StatusCode::NO_CONTENT.into_response(),
        }
    }
}

You can also pull the logic into your handler function, if you prefer. Response implements IntoResponse trivially, so you can write handlers like this:

async fn item_get() -> Response {
    let vehicle: Option<Vehicle> = todo!();
    match vehicle {
        Some(v) => (StatusCode::OK, Json(vehicle)).into_response(),
        None => StatusCode::NOT_FOUND.into_response(),
    }
}

I don't do this because it makes it harder for tests to inspect application-specific characteristics of the response, but it works just fine and if you're not testing handlers directly you might not be concerned about that.

4 Likes

Thanks very much for taking the time to show the full example, as well as the alternative option with the solution embedded into the route handler.

The latter option seems to be more in line with what I was hoping to accomplish. However, I can see why it would be preferable to implement the IntoResponse trait on a custom type. It is unfortunate that Rust doesn't allow implementation of traits on foreign types, requiring you to essentially redefine Option<T> again yourself.

To answer your question about my expected response, I was thinking:

  • If an object is returned, the response body / payload would contain the JSON representation of the vehicle, and return HTTP status code 200 OK
  • If no object is returned, then the API would return HTTP 204 No Content, and an empty body / payload

I had assumed that using the tuple return type would allow me to accomplish that by simply wrapping my custom type in Option<T>. However, based on my original post and your responses, I can see that's not possible.

I think the answer here is that I just need to be comfortable with implementing IntoResponse on custom types. Thanks again for sharing your insights.

You have misunderstood what despiny was trying to tell you. It's not Rust's type system's fault, it's the way you have architected your application.

If you are 100% sure you want Option<T> to model your domain, it's as simple as using a match or an if let statement in the API layer to choose the response that should come out from your request handler.

In other words, you should be aware of the constraints inherent to the way you model your data.