aws / aws/aws-lambda-rust-runtime

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

Open
#1,165 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
3.6k
Forks
396
PR merge metrics
No merged PRs in 30d

Description

## 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`:

```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`:

```rust
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:

```bash
cargo run
```

## Actual behavior

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

```text
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` 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:

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

However, both buffered conversion paths use `expect()`:

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

Source at the `lambda_http-v1.3.0` tag:

- [`convert_to_binary` and `convert_to_text`](https://github.com/aws/aws-lambda-rust-runtime/blob/lambda_http-v1.3.0/lambda-http/src/response.rs#L377-L420)
- [`expect()` in `convert_to_binary`](https://github.com/aws/aws-lambda-rust-runtime/blob/lambda_http-v1.3.0/lambda-http/src/response.rs#L387)
- [`expect()` in `convert_to_text`](https://github.com/aws/aws-lambda-rust-runtime/blob/lambda_http-v1.3.0/lambda-http/src/response.rs#L412)

## 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.

Contributor guide

Open the contributing guide

Research direction

Start in lambda-http/src/response.rs by reading convert_to_binary and convert_to_text, focusing on the body.collect() calls and their expect() handling. Run the minimal reproduction with cargo run to observe the panic. Done means a fallible body reports its original error without panicking in either buffered conversion path.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.