2

In a web server application using rocket.rs, I am using an Error type that implement Responder throughout my API. This error type ensures all errors are uniformly rendered (as RFC 7807 json).

However, I can't find a way to use these Error responses in RequestGuards. It seems that the from_request function results in an Outcome which uses an entirely different model, returning Outcome::Failure((Status, T)) on errors.

How can I ensure that errors in these request guards are rendered in the same JSON format? Is it even customizable?

I have tried to use a catcher, but this does not seem to retrieve any error information whatsoever.

Tomas
  • 5,067
  • 1
  • 35
  • 39

1 Answers1

6

The docs for FromRequest's Outcome state:

Note that users can request types of Result<S, E> and Option<S> to catch Failures and retrieve the error value.

  1. At the start of your FromRequest implementation, define type Error = JsonValue;

  2. In the from_request function, make sure it returns request::Outcome<S, Self::Error> where S is what you're implementing for.

  3. In the from_request function, when you want to return a failure do something like Outcome::Failure((Status::Unauthorized, json!({"error": "unauthorised"}))), or whatever it is you want to return.

  4. In your route's function use Result<S, JsonValue> as the type of the request guard, where S is what you where implementing for. In your route, use match to match it to Ok(S) or Err(json_error) for example.

There is probably a way to pass on the status of Outcome::Failure, but the solution I've described means if you're using a custom responder you would set the status in the responder, not based on Outcome::Failure - for example the code below.

Here's an example applied to the ApiKey request guard example from the docs, with an example custom responder called ApiResponse that sets its own status:

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

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

#[derive(Debug)]
pub struct ApiResponse {
    pub json: JsonValue,
    pub 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()
    }
}

#[derive(Debug, Deserialize, Serialize)]
struct ApiKey(String);

/// Returns true if `key` is a valid API key string.
fn is_valid(key: &str) -> bool {
    key == "valid_api_key"
}

impl<'a, 'r> FromRequest<'a, 'r> for ApiKey {
    type Error = JsonValue;

    fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
        let keys: Vec<_> = request.headers().get("x-api-key").collect();
        match keys.len() {
            0 => Outcome::Failure((Status::BadRequest, json!({ "error": "api key missing" }))),
            1 if is_valid(keys[0]) => Outcome::Success(ApiKey(keys[0].to_string())),
            1 => Outcome::Failure((Status::BadRequest, json!({ "error": "api key invalid" }))),
            _ => Outcome::Failure((Status::BadRequest, json!({ "error": "bad api key count" }))),
        }
    }
}

#[get("/sensitive")]
fn sensitive(key: Result<ApiKey, JsonValue>) -> ApiResponse {
    match key {
        Ok(_ApiKey) => ApiResponse {
            json: json!({ "data": "sensitive data." }),
            status: Status::Ok
        },
        Err(json_error) => ApiResponse {
            json: json_error,
            status: Status::BadRequest
        }
    }
}

I'm new to Rust and Rocket, so this might not be the best solution.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
SRugina
  • 129
  • 9