Introduction
In Hyper (and most Rust HTTP frameworks), the request body is a stream that can only be consumed once. When middleware logs or inspects the body and the handler also tries to read it, the second consumer gets an empty stream or the application panics. This is a common issue when implementing request logging, authentication middleware that validates request bodies, or debugging endpoints that need to inspect the raw body.
Symptoms
- Handler receives empty body after middleware logged it
error: body stream already consumedcompilation error- Request body appears empty in handler but was present in request
hyper::Bodyconsumed error in middleware chain- Panic when trying to collect body bytes twice
Example panic:
``
thread 'tokio-runtime-worker' panicked at 'body stream already consumed',
hyper-0.14.27/src/body/body.rs:234:13
Common Causes
- Logging middleware reads body bytes, handler gets empty stream
- Authentication middleware parses JSON body, handler cannot
- Body not buffered before multiple consumers need access
hyper::Bodydoes not implementClone- Tower layer reads body but does not restore it
Step-by-Step Fix
- 1.Buffer the body once and share it with middleware and handler:
- 2.```rust
- 3.use axum::{
- 4.body::Body,
- 5.extract::Request,
- 6.middleware::Next,
- 7.response::Response,
- 8.};
- 9.use http_body_util::BodyExt;
async fn log_body_middleware( request: Request, next: Next, ) -> Response { // Extract the body bytes, buffering the stream let (parts, body) = request.into_parts();
// Collect body bytes let bytes = body .collect() .await .expect("Failed to read body") .to_bytes();
// Log the body if let Ok(body_str) = std::str::from_utf8(&bytes) { tracing::info!("Request body: {}", body_str); }
// Reconstruct the request with a new body from the buffered bytes let request = Request::from_parts( parts, Body::from(bytes), // Create new body from bytes );
// Pass reconstructed request to next middleware/handler next.run(request).await } ```
- 1.Handle large bodies with size limits:
- 2.```rust
- 3.async fn log_body_with_limit(
- 4.request: Request,
- 5.next: Next,
- 6.) -> Response {
- 7.const MAX_LOG_BYTES: usize = 4096; // Only log first 4KB
let (parts, body) = request.into_parts(); let bytes = body .collect() .await .expect("Failed to read body") .to_bytes();
if bytes.len() > MAX_LOG_BYTES { tracing::info!( "Request body (truncated, {} bytes total): {:?}", bytes.len(), std::str::from_utf8(&bytes[..MAX_LOG_BYTES]) ); } else if let Ok(body_str) = std::str::from_utf8(&bytes) { tracing::info!("Request body: {}", body_str); }
let request = Request::from_parts(parts, Body::from(bytes)); next.run(request).await } ```
- 1.Skip body consumption for certain content types:
- 2.```rust
- 3.use mime::Mime;
async fn conditional_log_body( request: Request, next: Next, ) -> Response { let content_type = request.headers().get("content-type");
if !should_log { return next.run(request).await; }
// Log body for text-based content types let (parts, body) = request.into_parts(); let bytes = body.collect().await.unwrap().to_bytes();
if let Ok(body_str) = std::str::from_utf8(&bytes) { tracing::debug!("Request body: {}", body_str); }
let request = Request::from_parts(parts, Body::from(bytes)); next.run(request).await } ```
- 1.Use axum extractors that handle body consumption correctly:
- 2.```rust
- 3.use axum::{
- 4.extract::Json,
- 5.routing::post,
- 6.Router,
- 7.};
- 8.use serde::Deserialize;
#[derive(Deserialize)] struct CreateUser { name: String, email: String, }
// Axum handles body consumption correctly with extractors async fn create_user(Json(payload): Json<CreateUser>) -> String { // payload is already deserialized, body consumed once format!("Created user: {}", payload.name) }
// WRONG - trying to extract body twice async fn create_user_wrong( body: axum::body::Bytes, // Consumes body Json(payload): Json<CreateUser>, // Tries to consume again - fails! ) -> String { format!("Body: {:?}, User: {:?}", body, payload) } ```
- 1.Debug body consumption issues with Tower layer:
- 2.```rust
- 3.use tower::ServiceBuilder;
- 4.use tower_http::trace::TraceLayer;
let app = Router::new() .route("/api/users", post(create_user)) .layer( ServiceBuilder::new() // TraceLayer logs request/response metadata without consuming body .layer(TraceLayer::new_for_http()) );
// For body logging, use the custom middleware from step 1 // Do NOT use TraceLayer for body content - it does not read the body ```
Prevention
- Never read the body in middleware without reconstructing the request
- Use
BodyExt::collect()to buffer, then create a newBody::from(bytes) - Skip body logging for large uploads and binary content types
- Use Axum extractors (
Json,Form,Bytes) instead of manual body reading - Set body size limits to prevent memory exhaustion from buffering
- Document which middleware consumes the body for team awareness