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

how to return error as a Json from FromParam #2714

Closed
2 tasks done
mtumilowicz opened this issue Jan 27, 2024 · 3 comments
Closed
2 tasks done

how to return error as a Json from FromParam #2714

mtumilowicz opened this issue Jan 27, 2024 · 3 comments
Labels
docs Improvements or additions to documentation

Comments

@mtumilowicz
Copy link

mtumilowicz commented Jan 27, 2024

What kind of documentation problem are you reporting?

Technical Problem

Where is the issue found?

https://api.rocket.rs/v0.5/rocket/request/trait.FromParam.html

What's wrong?

what I would like to have: returning error in json format from FromParam

expectations

something like this, but using FromParam:

#[derive(Serialize)]
pub struct ErrorApiOutput(HashMap<&'static str, Vec<Cow<'static, str>>>);

pub fn for_key(key: &'static str, message: Cow<'static, str>) -> ErrorApiOutput {
    let mut data = HashMap::new();
    data.insert(key, vec![message]);

    ErrorApiOutput::new(data)
}

fn parse_customer_id(customer_id: &str) -> Result<CustomerId, Custom<Json<ErrorApiOutput>>> {
    match Uuid::parse_str(customer_id) {
        Ok(uuid) => Ok(CustomerId::new(uuid)),
        Err(_) => {
            let output = ErrorApiOutput::for_key("customer_id", Cow::Borrowed("is not a correct uuid"));
            Err(Custom(Status::UnprocessableEntity, Json(output)))
        }
    }
}

so: to parse correctly CustomerId or to return UnprocessableEntity with json body:

{
    "customer_id": [
        "is not a correct uuid"
    ]
}

code:

use rocket::{get, launch, routes};
use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client;
use rocket::request::FromParam;
use rocket::response::status::NotFound;
use rocket::serde::json::{Json, json};
use rocket::serde::uuid::Uuid;

struct CustomerId(Uuid);

#[derive(Debug)]
struct ErrorApiOutput {
    error: String
}

impl<'r> FromParam<'r> for CustomerId {
    type Error = Json<ErrorApiOutput>;

    fn from_param(param: &'r str) -> Result<Self, Self::Error> {
        match Uuid::parse_str(param) {
            Ok(uuid) => Ok(CustomerId(uuid)),
            Err(_) => Err(Json(ErrorApiOutput { error: "malformed uuid".to_string() })),
        }
    }
}

#[get("/customers/<customer_id>")]
pub fn get_customer(customer_id: Uuid) -> Result<String, NotFound<String>> {
    Ok(customer_id.to_string())
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/api", routes![get_customer])
}

#[test] // working
fn test_get_customer_correct_uuid() {
    let client = Client::tracked(rocket()).expect("valid Rocket instance");

    let response = client.get("/api/customers/550e8400-e29b-41d4-a716-446655440000").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.into_string(),
        Some("550e8400-e29b-41d4-a716-446655440000".to_string())
    );
}

#[test] // not working
fn test_get_customer_incorrect_uuid() {
    let client = Client::tracked(rocket()).expect("valid Rocket instance");

    let response = client.get("/api/customers/invalid-uuid").dispatch();

    assert_eq!(response.status(), Status::UnprocessableEntity);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(
        response.into_string(),
        Some(json!({"error": "malformed uuid"}).to_string())
    );
}

problem

I specified FromParam to parse String into CustomerId(Uuid); when it is not correct I want to show appropriate error as a Json

as I checked with curl: curl http://localhost:8000/api/customers/550e8400-e29b-41d4-a716-44665544000

html is returned, and it does not contain appropriate error message

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="color-scheme" content="light dark">
    <title>422 Unprocessable Entity</title>
</head>
<body align="center">
    <div role="main" align="center">
        <h1>422: Unprocessable Entity</h1>
        <p>The request was well-formed but was unable to be followed due to semantic errors.</p>
        <hr />
    </div>
    <div role="contentinfo" align="center">
        <small>Rocket</small>
    </div>
</body>
</html>

what is more - documentation does not specify how to return Json for that case

System Checks

  • I confirmed that the issue still exists on master on GitHub.
  • I was unable to find a previous report of this problem.
@mtumilowicz mtumilowicz added the docs Improvements or additions to documentation label Jan 27, 2024
@SergioBenitez
Copy link
Member

The easiest thing to do is this:

type ApiResult<T> = Result<T, Custom<Json<ErrorApiOutput>>>;

#[get("/customers/<customer_id>")]
pub fn get_customer(customer_id: ApiResult<CustomerId>) -> ApiResult<String> {
    let id = customer_id?;
    Ok(id.to_string())
}

@mtumilowicz
Copy link
Author

mtumilowicz commented Jan 30, 2024

working! thank u very much

@SergioBenitez do u think that we should update documentation? for me connection between FromParam and input type of param in endpoint was not clear

fully working code for ref:

use rocket::{launch};

use rocket::{get, routes};
use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client;
use rocket::request::FromParam;
use rocket::response::status::{Custom};
use rocket::serde::json::{Json, json};
use rocket::serde::uuid::Uuid;
use serde_derive::Serialize;

struct CustomerId(Uuid);

#[derive(Debug, Serialize)]
struct ErrorApiOutput {
    error: String,
}

impl<'r> FromParam<'r> for CustomerId {
    type Error = Custom<Json<ErrorApiOutput>>;

    fn from_param(param: &'r str) -> Result<Self, Self::Error> {
        match Uuid::parse_str(param) {
            Ok(uuid) => Ok(CustomerId(uuid)),
            Err(_) => Err(Custom(Status::UnprocessableEntity, Json(ErrorApiOutput { error: "malformed uuid".to_string() }))),
        }
    }
}

type ApiResult<T> = Result<T, Custom<Json<ErrorApiOutput>>>;

#[get("/customers/<customer_id>")]
pub fn get_customer(customer_id: Result<CustomerId, Custom<Json<ErrorApiOutput>>>) -> Result<String, Custom<Json<ErrorApiOutput>>> {
    let id = customer_id?;
    Ok(id.0.to_string())
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/api", routes![get_customer])
}

#[test] // working
fn test_get_customer_correct_uuid() {
    let client = Client::tracked(rocket()).expect("valid Rocket instance");

    let response = client.get("/api/customers/550e8400-e29b-41d4-a716-446655440000").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.into_string(),
        Some("550e8400-e29b-41d4-a716-446655440000".to_string())
    );
}

#[test] // working
fn test_get_customer_incorrect_uuid() {
    let client = Client::tracked(rocket()).expect("valid Rocket instance");

    let response = client.get("/api/customers/invalid-uuid").dispatch();

    assert_eq!(response.status(), Status::UnprocessableEntity);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(
        response.into_string(),
        Some(json!({"error": "malformed uuid"}).to_string())
    );
}

@SergioBenitez
Copy link
Member

This is already mentioned here: https://rocket.rs/v0.5/guide/requests/#fallible-guards. But I'll keep this in mind for future iteration of the docs.

P.S: You created a type alias that you then don't use. You should use it to clean up the code a bit.

@SergioBenitez SergioBenitez closed this as not planned Won't fix, can't repro, duplicate, stale Jan 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

2 participants