aws / aws/aws-lambda-rust-runtime
[lambda_http] Fallible response body panics during buffered conversion instead of propagating the body error
- Lenguaje dominante
- Rust
- Estrellas
- 3.6k
- Forks
- 396
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Descripción
## 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.
Guía de contribución
Línea de trabajo
Comienza en lambda-http/src/response.rs leyendo convert_to_binary y convert_to_text, centrándote en las llamadas a body.collect() y en el manejo de expect(). Ejecuta la reproducción mínima con cargo run para observar el panic. Está terminado cuando un body que puede fallar informa de su error original sin provocar un panic en ninguna de las rutas de conversión con búfer.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- rust
- Área
- api, backend
- Tipo de issue
- Error
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Estado de actividad
- Activo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 74/100