Streaming responses
Stream a large export or download chunk-by-chunk with StreamingResponse instead of buffering it in memory.
Streaming responses
A normal handler returns a fully-buffered body: a String, Html, or Json. That's right for a page, but a 200 MB CSV export or a file download shouldn't sit in memory all at once. StreamingResponse sends the body chunk-by-chunk from an async stream: memory stays flat regardless of size, and the client starts receiving bytes before the last row is generated.
StreamingResponse composes with compression: a streamed body is gzip/brotli-compressed on the fly by the same layer, so a huge ?format=csv both streams and compresses without buffering.
Infallible chunks
When producing a chunk can't fail (you're formatting rows you already have), use from_chunks. Each item is anything that converts to bytes: String, Bytes, Vec<u8>, &'static str:
use umbral::prelude::*; // brings in StreamingResponseuse futures_util::stream; async fn export_csv() -> StreamingResponse { let header = std::iter::once("id,name,total\n".to_string()); let rows = (0..1_000_000).map(|i| format!("{i},item-{i},{}\n", i * 10)); let body = stream::iter(header.chain(rows)); StreamingResponse::from_chunks(body) .content_type("text/csv; charset=utf-8") .attachment("export.csv") // browser offers a save dialog}Fallible chunks
When producing a chunk can fail (reading a DB row, a file read), use new with a stream of Result<impl Into<Bytes>, impl Into<BoxError>>. The status line and headers are sent as soon as the first chunk is ready, so an error partway through aborts the response: the client sees a truncated body, which is the honest outcome for an already-started stream:
use umbral::web::StreamingResponse;use axum::body::Bytes; async fn download_file(path: std::path::PathBuf) -> StreamingResponse { let file = tokio::fs::File::open(path).await.unwrap(); let reader = tokio_util::io::ReaderStream::new(file); // Stream<Item = io::Result<Bytes>> StreamingResponse::new(reader) .content_type("application/pdf") .inline("invoice.pdf") // display in-browser, suggest a name}Builder options
| Method | Effect |
|---|---|
.content_type(ct) | Set Content-Type (default application/octet-stream). |
.attachment(name) | Content-Disposition: attachment; filename="<name>". Download. |
.inline(name) | Content-Disposition: inline; filename="<name>". Display, suggest a name. |
.status(code) | Override the status (default 200 OK). |
Filenames passed to .attachment / .inline are stripped of CR, LF, and " so they can't inject extra response headers or break the quoted header value.
See also
- Compression - gzip/brotli, which wraps streamed bodies transparently.
- CSV export - the REST list endpoint's
?format=csv. crates/umbral-core/src/web/streaming.rsfor the implementation.