How do I parse a JSON body in Rocket when the content type is not "application/json"?

1.2k views Asked by At

I'm trying to parse this JSON CSP Record being submitted via POST directly by the browser into a nested struct:

{"csp-report":{"document-uri":"http://localhost:8000/demo/","referrer":"","violated-directive":"img-src","effective-directive":"img-src","original-policy":"default-src 'self'; report-uri /.well-known/csp-violation","disposition":"report","blocked-uri":"https://www.google.com/logos/doodles/2020/googles-22nd-birthday-6753651837108550-law.gif","line-number":47,"source-file":"http://localhost:8000/demo/","status-code":200,"script-sample":""}}

The following headers are sent:

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 442
Content-Type: application/csp-report
Host: localhost:8000
Origin: http://localhost:8000
Pragma: no-cache
Referer: http://localhost:8000/demo/
Sec-Fetch-Dest: report
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36

I followed the Rocket JSON data guide, but the request generates an Unprocessable Entity (422) and I don't know why.

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
struct Report {
    #[serde(with = "serde_with::json::nested")]
    csp_report: ReportBody,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
struct ReportBody {
    blocked_uri: String,
    disposition: String,
    document_uri: String,
    effective_directive: String,
    line_number: u128,
    original_policy: String,
    referrer: String,
    script_sample: String,
    source_file: String,
    status_code: u16,
    violated_directive: String,
}

#[post(
    "/.well-known/csp-violation",
    format = "application/csp-report",
    data = "<_report>"
)]
fn record(_report: Json<Report>) -> Status {
    Status::NoContent
}

fn main() {
    rocket::ignite().mount("/", routes![record]).launch();
}

My guess is that it's due to the header Content-Type: application/csp-report which I can't change, because the browser sends the report automatically.

1

There are 1 answers

0
jla On BEST ANSWER

JSON from rocket_contrib is a convenience, but not essential. You can still parse the JSON yourself from the raw body data with serde (following example is done with async Rocket). This may bypass any issues with headers:

use rocket::Data;
...

#[post(
    "/.well-known/csp-violation",
    data = "<data>"
)]
async fn record(data: Data) -> Status {
    let body = match data.open(128.kilobytes()).stream_to_string().await {
        Ok(d) => d,
        Err(_) => return Status::NoContent, // or whatever error
    };
    let report: Report = match serde_json::from_str(&body) {
        Ok(a) => a,
        Err(_) => return Status::NoContent, // or whatever error
    };
    // do what you want with report here
    ...
    
    Status::NoContent
}