19

I want my Rocket API to have a route like this:

#[post("create/thing", format = "application/json", data="<thing>")]

When the client sends { "name": "mything" }, everything should be alright and I know how to do that, but when it sends { "name": "foo" } it should respond with something like this:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
  "errors": [
    {
      "status": "422",
      "title":  "Invalid thing name",
      "detail": "The name for a thing must be at least 4 characters long."
    }
  ]
}

How do I respond with a result like a JSON object and a HTTP status code different than 200 in Rocket?

This is what I tried so far:

  • impl FromRequest for my Thing type. This lets me choose a status code as I can write my own from_request function, but I can't return anything else.
  • Registering an error catcher like in this example, but this way I only can react to one HTTP status code without context. I have too many failure modes to reserve one HTTP status code for each.
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
erictapen
  • 578
  • 3
  • 13
  • Does https://rocket.rs/v0.4/guide/responses/#responses help? – hellow Feb 25 '19 at 12:10
  • @hellow I actually looked into it and decided, that it would not help, but now I got it working with a Responder impl! Will provide an answer in the next hour, thanks. – erictapen Feb 25 '19 at 12:55
  • I already tried to do the same, but I didn't find the answer. Maybe that's impossible. – Boiethios Feb 25 '19 at 13:11

2 Answers2

13

With @hellow's help, I figured it out. The solution is to implement the Responder trait for a new struct ApiResponse, which contains a status code as well the Json. This way I can do exactly what I wanted:

#[post("/create/thing", format = "application/json", data = "<thing>")]
fn put(thing: Json<Thing>) -> ApiResponse {
    let thing: Thing = thing.into_inner();
    match thing.name.len() {
        0...3 => ApiResponse {
            json: json!({"error": {"short": "Invalid Name", "long": "A thing must have a name that is at least 3 characters long"}}),
            status: Status::UnprocessableEntity,
        },
        _ => ApiResponse {
            json: json!({"status": "success"}),
            status: Status::Ok,
        },
    }
}

Here is the full code:

#![feature(proc_macro_hygiene)]
#![feature(decl_macro)]

#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use rocket::http::{ContentType, Status};
use rocket::request::Request;
use rocket::response;
use rocket::response::{Responder, Response};
use rocket_contrib::json::{Json, JsonValue};

#[derive(Serialize, Deserialize, Debug)]
pub struct Thing {
    pub name: String,
}

#[derive(Debug)]
struct ApiResponse {
    json: JsonValue,
    status: Status,
}

impl<'r> Responder<'r> for ApiResponse {
    fn respond_to(self, req: &Request) -> response::Result<'r> {
        Response::build_from(self.json.respond_to(&req).unwrap())
            .status(self.status)
            .header(ContentType::JSON)
            .ok()
    }
}

#[post("/create/thing", format = "application/json", data = "<thing>")]
fn put(thing: Json<Thing>) -> ApiResponse {
    let thing: Thing = thing.into_inner();
    match thing.name.len() {
        0...3 => ApiResponse {
            json: json!({"error": {"short": "Invalid Name", "long": "A thing must have a name that is at least 3 characters long"}}),
            status: Status::UnprocessableEntity,
        },
        _ => ApiResponse {
            json: json!({"status": "success"}),
            status: Status::Ok,
        },
    }
}

fn main() {
    rocket::ignite().mount("/", routes![put]).launch();
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
erictapen
  • 578
  • 3
  • 13
  • 2
    From Rocket 0.5.0, `rocket_contrib` won't be needed for this. You'll be able to `use rocket::serde::json::{json, Json, Value};`, `Value` being a replacement for `JsonValue`. – Drarig29 Jun 27 '21 at 15:39
  • I not managed to use it with rocket 5.0rc1. but with 0.4.9 its works fine. Thanks! Returning JSON payload to Error status pain in the A. with Rocket. – Boris Ivanov Aug 23 '21 at 16:37
  • In rocket 5.0rc1 Respoder needs two lifetimes, like this: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=32b669857a77eba19a17aa68c89a2393 – Cirelli94 May 05 '22 at 12:13
5

You need to build a response. Take a look at the ResponseBuilder. Your response might look something like this.

use std::io::Cursor;
use rocket::response::Response;
use rocket::http::{Status, ContentType};

let response = Response::build()
    .status(Status::UnprocessableEntity)
    .header(ContentType::Json)
    .sized_body(Cursor::new("Your json body"))
    .finalize();
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Januson
  • 4,533
  • 34
  • 42