Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Streaming Responses

Return a response body incrementally instead of buffering it all in memory. Useful for large downloads, proxied bodies, and as the foundation for Server-Sent Events.

Streaming is always available — no Cargo feature required.

ctx.stream(...)

ctx.stream takes a Stream of byte chunks and sends them as they arrive. The response is chunked (no Content-Length).

use hyper::body::Bytes;
use ultimo::prelude::*;
use ultimo::response::BoxError;
 
async fn numbers(ctx: Context) -> Result<Response> {
    // Any `Stream<Item = Result<Bytes, BoxError>>` works.
    let chunks: Vec<std::result::Result<Bytes, BoxError>> =
        (0..5).map(|n| Ok(Bytes::from(format!("line {n}\n")))).collect();
    ctx.stream(futures_util::stream::iter(chunks)).await
}

Set a content type first if you need one:

async fn csv(ctx: Context) -> Result<Response> {
    ctx.header("Content-Type", "text/csv").await;
    let rows: Vec<std::result::Result<Bytes, BoxError>> = vec![
        Ok(Bytes::from("a,b,c\n")),
        Ok(Bytes::from("1,2,3\n")),
    ];
    ctx.stream(futures_util::stream::iter(rows)).await
}

Errors

If a stream item yields Err(_), the response connection is aborted. Emit Ok(_) chunks for normal data and reserve Err(_) for genuine failures.

Interaction with compression

The compression middleware skips streaming bodies — it never buffers a stream (that would defeat streaming and risk unbounded memory). Buffered responses (text/json/html) are still compressed as usual.

The body type

Response is hyper::Response<UltimoBody>. UltimoBody::Full is the buffered fast path (what text/json/html produce); UltimoBody::Stream is what ctx.stream produces. You rarely construct it directly.

Full example

See examples/streaming: cargo run -p streaming-example, then open http://127.0.0.1:3000.