Skip to content

Option<T> guard should fail if T fails, not be None #2543

@djrenren

Description

@djrenren

Rocket Version: 0.5.0-rc.3

Description

FromData implementation for Option<T> causes malformed bodies to be silently ignored.

To Reproduce

Sometimes a route handler needs to support an optional request body (for example, when making a backwards-compatible addition to an endpoint that previously required no request body).

So the definition of such a route looks like so:

#[post("/my_route", data = "<body>")]
pub async fn my_route_handler(body: Option<Json<Foo>>) {
    /* ... */
}

The desired behavior is that, if no request data is present, we will get None for the body, and if some data is provided, we'll try to parse it. This is mostly how it works except, in the case where the body does not represent a valid Json<Foo> that is, Json::<Foo>::from_data returns a Failure result. Rather than a failure, we get None.

Expected Behavior

If a request body is supplied, it should be validated.

Environment:

  • OS Distribution and Kernel: all
  • Rocket Version: 0.5.0-rc.3

Additional Context

For prior art, we can look at what serde does. In general, serde follows the model that optional values are validated if supplied, so the following fails:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
enum Foo {
    MyType
}

#[derive(Debug, Deserialize)]
struct Test {
    #[serde(default)]
    field: Option<Foo>
}

fn main() {
    serde_json::from_str::<Test>(r#"{"field": "garbage"}"#).unwrap();
}

Playground

That is, because a value was provided for field, an incorrect value results in an error, not a silent None.

It seems to me that rocket should follow this example because it provides stronger guarantees and more information.
If users want infallible parsing, they could use Result and just ignore any errors.

Proposal for updating FromData impl

Currently, the implementation of FromData for Option<T> clearly swallows all errors:

impl<'r, T: FromData<'r>> FromData<'r> for Option<T> {
    type Error = std::convert::Infallible;

    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
        match T::from_data(req, data).await {
            Success(v) => Success(Some(v)),
            Failure(..) | Forward(..) => Success(None),
        }
    }
}

I propose this safer implementation of FromData:

/// Unsure how to write this function body cuz I'm not as familiar with rocket internals.
fn is_empty(data: &Data<'_>) -> bool { /* ... */}

impl<'r, T: FromData<'r>> FromData<'r> for Option<T> {
    type Error = T::Error;

    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
        if is_empty(data) {
            return None;
        }

       T::from_data(req, data).await.map(Some)
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    suggestionA suggestion to change functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions