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

Document or provide examples for more complex error handling #34

Open
kov opened this issue Aug 14, 2020 · 6 comments
Open

Document or provide examples for more complex error handling #34

kov opened this issue Aug 14, 2020 · 6 comments
Labels
documentation Improvements or additions to documentation

Comments

@kov
Copy link

kov commented Aug 14, 2020

Hello! I am new to Rust and have recently learned about okapi. I am trying to use it for a REST API with a mongodb backend. I would like to properly handle and report mongodb-related failures as a HTTP 500 error, but I cannot figure out how to do that. I suppose I could implement OpenApiResponder for the mongodb errors, but it is not clear to me how to properly implement or how I would set up the return type for my handler. An example or more documentation would be very helpful =)

@ralpha
Copy link
Collaborator

ralpha commented Aug 14, 2020

If you use rocket you can catch general errors using:
https://api.rocket.rs/v0.4/rocket/struct.Catcher.html
Note that the code below will NOT use this as it already handles the error case.

#[derive(Serialize, Deserialize)]
pub struct APIErrorResponse {
    error: APIError,
}
#[catch(500)]
pub fn internal_error() -> Json<APIErrorResponse> {
    Json(APIErrorResponse {
             ....some struct info...
        }
    })
}

For the okapi part:

/// Example response message, This will be added to the openapi file
fn add_500_error(responses: &mut Responses) {
    responses
        .responses
        .entry("500".to_owned())
        .or_insert_with(|| {
            let mut response = okapi::openapi3::Response::default();
            response.description = format!(
                "\
                # [500 Internal Server Error]\
                (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500)\n\
                This response is given when the server has an internal error that it could not \
                recover from.\n\n\
                If you get this response please report this as an [issue here]\
                ({}).\
                ",
                "...some link..."
            );
            response.into()
        });
}

#[derive(Debug, Default)]
pub struct APIErrorNotModified {
    pub err: Option<Error>,
}

impl APIErrorNotModified {
    pub fn new() -> Self {
        APIErrorNotModified::default()
    }
}

/// Just to convert Errors to `failure::Error`
impl From<Error> for APIErrorNotModified {
    fn from(error: Error) -> Self {
        APIErrorNotModified { err: Some(error) }
    }
}

/// This is for Rocket to give the proper response
impl<'r> Responder<'r> for APIErrorNotModified {
    fn respond_to(self, _: &Request) -> response::Result<'r> {
        if let Some(error) = self.err {
            error!("{}", error); // this is for logging
            let error_response = APIErrorResponse {
                error: APIError {
                    message: error.to_string(),
                    issue_link: None,
                    code: 500,
                },
            };
            return Response::build()
                .header(ContentType::JSON)
                .status(Status::InternalServerError)
                .sized_body(std::io::Cursor::new(
                    serde_json::to_string(&error_response).unwrap(),
                ))
                .ok();
        }
        Response::build().status(Status::NotModified).ok()
    }
}

/// This is for okapi to add all the responses
impl OpenApiResponder<'_> for APIErrorNotModified {
    fn responses(_: &mut OpenApiGenerator) -> OpenApiResult<Responses> {
        let mut responses = Responses::default();
        add_304_error(&mut responses);
        add_404_error(&mut responses);
        add_500_error(&mut responses);
        Ok(responses)
    }
}

It just so happens that my code is made public today, so you can view my implementation here:
https://gitlab.com/df_storyteller/df-storyteller/-/blob/master/df_st_api/src/api_errors.rs
You can find various other things there that might be useful.
you can view the result here: https://docs.dfstoryteller.com/rapidoc/

You Rocket code can then just return a Result with this error.

/// Request a list of all entities
#[openapi]
#[get("/entities?<pagination..>")]
pub fn list_entities(
    conn: DfStDatabase,
    pagination: ApiPagination,
    server_info: State<ServerInfo>,
) -> Result<Json<MyApiPageResult>, APIErrorNotModified> {
   ....
}

If you have questions let me know.
I know this should be in the docs. Maybe I'll add it there if I have some time.

@kov
Copy link
Author

kov commented Aug 18, 2020

I ended up finding this https://github.com/Geigerkind/LegacyPlayersV3.git which I used as an example to write a custom BackendError like this:

#[derive(Clone, Debug, JsonSchema)]
pub enum BackendError {
  Bson(String),
  Database(String),
  NotFound
}

impl Responder<'static> for BackendError {
    fn respond_to(self, _: &Request) -> Result<Response<'static>, Status> {
        let body;
        let status = match self {
            BackendError::Bson(msg) => {
            body = msg.clone();
            Status::new(500, "Bson")
          },
          BackendError::Database(msg) => {
            body = msg.clone();
            Status::new(500, "Database")
          },
          BackendError::NotFound => {
            body = String::new();
            Status::NotFound
          }
        };
        Response::build()
          .status(status)
          .sized_body(Cursor::new(body))
          .ok()
        }
}

impl OpenApiResponder<'static> for BackendError {
    fn responses(gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
        let mut responses = Responses::default();
        let schema = gen.json_schema::<String>();
        add_schema_response(&mut responses, 500, "text/plain", schema.clone())?;
        add_schema_response(&mut responses, 404, "text/plain", schema.clone())?;
        Ok(responses)
    }
}

Then I can make the return for my openapi fns Result<Json, BackendError>, and I can provide some detail whether it was a bson failure or a mongodb problem, etc. Does that make sense?

@kov
Copy link
Author

kov commented Aug 18, 2020

@ralpha also, thanks for the reference to your code, lots for me to learn there! The ApiObject trait / ApiPage idea looks very interesting/convenient. Have you considered turning that into a crate? ;D

@ralpha ralpha added the documentation Improvements or additions to documentation label Sep 12, 2021
@serg-temchenko
Copy link

Hey! I have similar issue. Thank you all for this examples. But I have question about how it can be dynamically used? I mean each endpoint can have it's specific list of possible errors (like get method of the api can have only 401 error, but post can have 401 and 400 for example) but in this examples we have common error type (e.g. BackendError) and by the logic of OpenApiResponder the responses method doesn't have access to the context (self) it means that you can't dynamically define list of responses for specific endpoints (e.g. api method get will have the same response types that post has?) but I need have capability to define list of responses depends on endpoint logic. I can see only one way and this is bunch of boilerplate (repeating) code where you will define separate response type for each endpoint with the list of errors related to this specific case? its really feels like lack of access to self from OpenApiResponder::responses trait method...

@ralpha
Copy link
Collaborator

ralpha commented Feb 15, 2022

If you just need 1 for all get and 1 for all post, just create 2 error types. (GetBackendError and PostBackendError)

If you need true custom responses I would consider using Generics (BackendError<Http500Response>) or macros (backend_error!(401,500)).

its really feels like lack of access to self from OpenApiResponder::responses trait method...

This can never happen. Because self does not exists at this point. When server starts the openapi.json file is generated (kind of). At this point no requests would have come in. So an error struct/object was never created. The only thing you have access to is the type at that point.

But yes I know what you mean and no there is not really a solution for that (except for the solutions on top). In the APIs I have created there are usually a few responses that are possible (according to the documentation) but in reality some can never happen. But then again you could also return error codes that are not documented.

I would recommend that for APIs you just document that more error codes are allowed and document them. This will also force the consumers to reuse there error handling code for all endpoints and not just catch 1-2 error codes and not be prepared for others.

Just because it might be of use there are some of codes my API's can return:

  • 200 Ok
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 422 Unprocessable Entity (for json body errors)
  • 500 Internal Server Error
  • 503 Service Unavailable (service will never return this, because ...well..., this error means service is not there, so check loadbalancer or something else that would return this error)

@ralpha
Copy link
Collaborator

ralpha commented Feb 15, 2022

@ralpha also, thanks for the reference to your code, lots for me to learn there! The ApiObject trait / ApiPage idea looks very interesting/convenient. Have you considered turning that into a crate? ;D

Yes, but not any time soon {{he replies 1,5 years later 😅}}. I might at some point, but I would still do it slightly different then I did there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants