rwf2/Rocket · error · rocket::serde::json::Error

UnexpectedEof

UnexpectedEof

Error message

data limit exceeded

What it means

Runtime error from the Json<T> FromData guard (core/lib/src/serde/json.rs): the request body is opened with the 'json' limit (Limits::JSON, default 1 MiB); if the stream is truncated at the limit (into_string returns an incomplete Capped), Rocket returns io::ErrorKind::UnexpectedEof 'data limit exceeded', and the FromData impl maps it to 413 PayloadTooLarge. This is Rocket's guard against unbounded JSON bodies, not a client parsing error.

Source

Thrown at core/lib/src/serde/json.rs:187

    /// ```
    #[inline(always)]
    pub fn into_inner(self) -> T {
        self.0
    }
}

impl<'r, T: Deserialize<'r>> Json<T> {
    fn from_str(s: &'r str) -> Result<Self, Error<'r>> {
        serde_json::from_str(s).map(Json).map_err(|e| Error::Parse(s, e))
    }

    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Result<Self, Error<'r>> {
        let limit = req.limits().get("json").unwrap_or(Limits::JSON);
        let string = match data.open(limit).into_string().await {
            Ok(s) if s.is_complete() => s.into_inner(),
            Ok(_) => {
                let eof = io::ErrorKind::UnexpectedEof;
                return Err(Error::Io(io::Error::new(eof, "data limit exceeded")));
            },
            Err(e) => return Err(Error::Io(e)),
        };

        Self::from_str(local_cache!(req, string))
    }
}

#[crate::async_trait]
impl<'r, T: Deserialize<'r>> FromData<'r> for Json<T> {
    type Error = Error<'r>;

    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
        match Self::from_data(req, data).await {
            Ok(value) => Outcome::Success(value),
            Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => {
                Outcome::Error((Status::PayloadTooLarge, Error::Io(e)))
            },

View on GitHub (pinned to 3a54d079ae)

Solutions

  1. Raise the limit in Rocket.toml: [default.limits] json = "5 MiB"
  2. Redesign the API to stream large payloads (multipart with TempFile, or chunked upload endpoints) instead of huge JSON
  3. Handle the 413 in a catcher or client-side by splitting the request
  4. Confirm with curl --data-binary @big.json -H 'Content-Type: application/json' which side truncates

Example fix

# before
# Rocket.toml (json defaults to 1 MiB)
#[post("/report", data = "<r>")]
fn report(r: Json<Report>) { }

# after
# Rocket.toml
[default.limits]
json = "8 MiB"

#[post("/report", data = "<r>")]
fn report(r: Json<Report>) { }
Defensive patterns

Strategy: try-catch

Validate before calling

// route around the guard for oversized payloads: check Content-Length first
#[post("/report", data = "<data>")]
fn report(req: &Request<'_>, data: Data<'_>) -> Status {
    let limit: usize = 5 * 1024 * 1024;
    if let Some(len) = req.headers().get_one("Content-Length").and_then(|v| v.parse().ok()) {
        if len > limit { return Status::PayloadTooLarge; }
    }
    // small enough: continue (guard would also work here)
    Status::Ok
}

Try / catch

// client side: handle 413 by splitting or compressing the payload
match client.post("/report").json(&body).send().await {
    resp if resp.status() == StatusCode::PAYLOAD_TOO_LARGE => { /* split/retry smaller */ }
    resp => { /* normal path */ }
}

Prevention

When it happens

Trigger: POST/PUT with Content-Type: application/json and a body larger than limits.json (default 1 MiB) to a route guarded by Json<T>. The guard fails before serde deserialization ever runs, yielding 413.

Common situations: APIs accepting base64-encoded images or large arrays in JSON; batch endpoints; front-ends that grew payloads over time and only now crossed 1 MiB; proxies that don't enforce their own body limit so Rocket's fires first.

Related errors


AI-assisted analysis of rwf2/Rocket@3a54d079ae (2026-08-16). Data as JSON: /api/errors/7f65140fe4551d38. Report an issue: GitHub.