Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return a custom error from an openapi endpoint #244

Closed
ufoscout opened this issue Mar 22, 2022 · 9 comments
Closed

Return a custom error from an openapi endpoint #244

ufoscout opened this issue Mar 22, 2022 · 9 comments
Labels
enhancement New feature or request

Comments

@ufoscout
Copy link

ufoscout commented Mar 22, 2022

I have some endpoints implemented in poem that I would like to migrate to poem-openapi.
All the endpoints return a Result with the same custom error implementing ResponseError.
For example:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum CustomError {
    #[error("BadRequest")]
    BadRequest,
    #[error("Internal")]
    Internal,
}

impl ResponseError for CustomError {
    fn status(&self) -> StatusCode {
        match self {
            CustomError::BadRequest => StatusCode::FORBIDDEN,
            CustomError::Internal =>  StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

#[handler]
async fn get() -> Result<(), CustomError> {
    Ok(())
}

This works fine in poem; nevertheless, this does not compile with poem-openapi:

struct Api;

//Error: the trait bound `Result<(), CustomError>: ApiResponse` is not satisfied
#[OpenApi] 
impl Api {

    #[oai(path = "/ok", method = "get")]
    async fn web_nok(&self) -> Result<(), CustomError> {
        Ok(())
    }

}

I can make it compile with this workaround:

struct Api;

#[OpenApi] 
impl Api {

    #[oai(path = "/ok", method = "get")]
    async fn web_nok(&self) -> poem::Result<()> {
        Err(CustomError::BadRequest)?
    }

}

But, in this case, the response type and code are not mapped in the swagger UI.
Is there a way to share the same custom error between the two different types of endpoints and expose the expected types and code in the swagger UI?

@ufoscout ufoscout added the enhancement New feature or request label Mar 22, 2022
@Christoph-AK
Copy link
Contributor

Christoph-AK commented Mar 22, 2022

See #230 (comment)

I have a function in my error struct to return the error as Json<Whatever>.

@ufoscout
Copy link
Author

@Christoph-AK thanks, but I wonder if there's a more straightforward way of doing it.

@ufoscout
Copy link
Author

ufoscout commented Mar 23, 2022

@sunli829 Would it be possible to allow an OpenApi endpoint to return whatever result with an Error that implements ApiResponse?
So it would be possible to write:

use poem::web::Json;
use poem_openapi::{OpenApi, ApiResponse};
use serde::Serialize;
use thiserror::Error;


#[derive(Error, Debug)]
pub enum CustomError {
    #[error("BadRequest")]
    BadRequest,
    #[error("Internal")]
    Internal,
}

#[derive(ApiResponse)]
pub enum ApiErrorResponse {
    #[oai(status = 400)]
    BadRequest,
    #[oai(status = 500)]
    InternalServerError,
}

impl From<CustomError> for ApiErrorResponse {
    fn from(err: CustomError) -> Self {
        match err {
            CustomError::BadRequest => ApiErrorResponse::BadRequest,
            CustomError::Internal =>  ApiErrorResponse::InternalServerError,
        }
    }
}

#[derive(Serialize)]
struct MyJson {}

struct Api;

#[OpenApi] 
impl Api {

    // Here you can return a result
    // If the result is Ok(), then the response status code is 200 and the type is Json<MyJson>
    // If the result is Err(), then the response status code and type are defined by ApiErrorResponse
    #[oai(path = "/json", method = "get")]
    async fn get_json(&self) -> Result<Json<MyJson>, ApiErrorResponse> {
        // You can now use the ? operator
        Err(CustomError::BadRequest)?;
        Ok(Json(MyJson{}))
    }

}

I think this would be very ergonomic to use.

@sunli829
Copy link
Collaborator

sunli829 commented Mar 23, 2022

I think it's a good idea, will try it right now!

@sunli829
Copy link
Collaborator

Released in v1.3.17

Here is the example:

async fn test(&self) -> Result<(), MyResponse> {

@ufoscout
Copy link
Author

@sunli829
Thanks for this! I am testing 1.3.17 and, although it compiles, it does not generate the expected response codes and types in the swagger UI:
Screenshot_20220323_141304

In fact, I expected to find also the 400 and 500 status codes and related data schemas.

Here is the full code of my attempt:

use poem::{http::StatusCode, error::ResponseError, Server, Route, listener::TcpListener};
use poem_openapi::{OpenApi, OpenApiService, ApiResponse, Object, payload::Json };
use serde::Serialize;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum CustomError {
    #[error("BadRequest")]
    BadRequest,
    #[error("Internal")]
    Internal,
}

impl ResponseError for CustomError {
    fn status(&self) -> StatusCode {
        match self {
            CustomError::BadRequest => StatusCode::BAD_REQUEST,
            CustomError::Internal =>  StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

#[derive(Serialize, Object)]
pub struct MyJson {
    a: String
}

#[derive(Serialize, Object, Debug)]
pub struct InternalServerErrorPayload {
    message: String
}

#[derive(ApiResponse)]
pub enum ApiJsonResponse {
    #[oai(status = 200)]
    Ok(Json<MyJson>),
}

#[derive(ApiResponse, Debug)]
pub enum ApiErrorResponse {
    #[oai(status = 400)]
    BadRequest,
    #[oai(status = 500)]
    InternalServerError(Json<InternalServerErrorPayload>),
}

impl From<CustomError> for ApiErrorResponse {
    fn from(err: CustomError) -> Self {
        match err {
            CustomError::BadRequest => ApiErrorResponse::BadRequest,
            CustomError::Internal =>  ApiErrorResponse::InternalServerError(Json(InternalServerErrorPayload{ message: "something bad".to_owned()})),
        }
    }
}

struct Api;

#[OpenApi] 
impl Api {
    #[oai(path = "/json", method = "get")]
    async fn get_json(&self) -> Result<ApiJsonResponse, ApiErrorResponse> {
        // You can now use the ? operator
        Err(CustomError::BadRequest)?;
        Ok(ApiJsonResponse::Ok(Json(MyJson{a: "hello".to_owned()})))
    }
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    if std::env::var_os("RUST_LOG").is_none() {
        std::env::set_var("RUST_LOG", "poem=debug");
    }

    let api_service =
        OpenApiService::new(Api, "Hello World", "1.0").server("http://localhost:3000/api");
    let ui = api_service.swagger_ui();

    Server::new(TcpListener::bind("localhost:3000"))
        .run(Route::new().nest("/api", api_service).nest("/", ui))
        .await
}

@sunli829
Copy link
Collaborator

Since Rust does not yet support specialization, this requires some macro changes. I'll add it tomorrow.

@sunli829
Copy link
Collaborator

Released in v1.3.18 😁

@ufoscout
Copy link
Author

@sunli829 Amazing!! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants