Skip to content

[lambda_http] Fallible response body panics during buffered conversion instead of propagating the body error #1165

Description

@ysj101

Description

lambda_http accepts response body types whose HttpBody::Error is fallible, but the buffered response conversion calls expect() when collecting the body.

If the body returns an error—for example, when a proxied HTTP/1.1 chunked response is truncated—IntoResponse::into_response() panics instead of propagating the body error. This affects both the text and binary conversion paths.

When the handler runs under lambda_http::run, the panic may be caught by lambda_runtime's CatchPanicService and reported as an invocation error, so the process can survive when built with the default panic = "unwind" strategy.

However, this relies on catch_unwind as the error channel instead of propagating the body error:

  • With panic = "abort", which may be used for size-optimized Lambda builds, the process terminates.
  • The diagnostic says that the user handler panicked even though the panic originates inside lambda_http.
  • The structured body error is flattened into a panic message.
  • Outside lambda_http::run, such as in custom runtimes, tests, or other adapters, calling IntoResponse::into_response() directly panics the process, as demonstrated below.

Versions

  • lambda_http: 1.3.0
  • No modifications to lambda_http
  • The same behavior is present in earlier releases
  • rustc: 1.88.0 (6b00bc388 2025-06-23)
  • cargo: 1.88.0 (873a06493 2025-05-10)
  • OS: Debian GNU/Linux 12 (bookworm)
  • Architecture: aarch64
  • Reproduction environment: official rust:1.88-bookworm Docker image

Minimal reproduction

Cargo.toml:

[package]
name = "lambda-http-body-error-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
lambda_http = "=1.3.0"
bytes = "1"
futures-util = "0.3"
http-body = "1"
http-body-util = "0.1"
tokio = { version = "1", features = ["macros", "rt"] }

src/main.rs:

use bytes::Bytes;
use futures_util::stream;
use http_body::Frame;
use http_body_util::StreamBody;
use lambda_http::{IntoResponse, Response};
use std::io::{self, ErrorKind};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let frames = vec![
        Ok(Frame::data(Bytes::from_static(b"partial response"))),
        Err(io::Error::new(
            ErrorKind::UnexpectedEof,
            "simulated truncated response body",
        )),
    ];
    let body = StreamBody::new(stream::iter(frames));

    let response = Response::builder()
        .header("content-type", "text/plain; charset=utf-8")
        .body(body)
        .unwrap();

    // Panics in lambda_http::response::convert_to_text().
    let _ = response.into_response().await;
}

Run:

cargo run

Actual behavior

The process exits with status 101 and produces the following panic:

thread 'main' panicked at /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/lambda_http-1.3.0/src/response.rs:412:42:
unable to read bytes from body: Custom { kind: UnexpectedEof, error: "simulated truncated response body" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

A real transport-level example is an HTTP proxy returning a Response<hyper::body::Incoming> where the upstream server closes an HTTP/1.1 chunked response before completing the current chunk.

Hyper exposes the premature closure as a body error. That error reaches the same expect() call in the buffered response conversion and is converted into a panic.

Expected behavior

A response body error should be propagated as an error rather than surfaced through panic! and catch_unwind, so that:

  • the failure is reported as an invocation error regardless of the configured panic strategy;
  • the original body error is preserved in the diagnostic instead of being flattened into a panic message attributed to the user handler;
  • callers of IntoResponse outside lambda_http::run are not taken down by a request-level I/O failure.

If the existing IntoResponse API cannot propagate the error without an API change, returning a deterministic error response from the conversion would still be preferable to panicking.

Affected code

ConvertBody explicitly accepts fallible body types, with bounds including:

B: HttpBody + Unpin + Send + 'static,
B::Data: Send,
B::Error: fmt::Debug,

However, both buffered conversion paths use expect():

body.collect()
    .await
    .expect("unable to read bytes from body")

Source at the lambda_http-v1.3.0 tag:

Impact

A single malformed or prematurely closed upstream response turns a request-level I/O failure into a panic inside the runtime's response conversion path.

This is especially relevant for HTTP adapters and reverse proxies that return a live, fallible HttpBody, because transport-level failures are expected to be represented and handled as normal body errors.

In environments using panic = "abort", this can also terminate the runtime process instead of failing only the affected invocation.

Related issue

This appears to be different from #1051.

That issue concerns an early client closure in the streaming runtime path involving lambda_runtime and send_data().unwrap() under local Lambda emulation.

This report concerns lambda_http::IntoResponse collecting a fallible application response body in the buffered conversion path.

The minimal reproduction does not require:

  • Lambda response streaming;
  • a client disconnect;
  • a Lambda emulator;
  • an actual HTTP server.

It only requires a valid HttpBody implementation that returns an error while its frames are being collected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions