Streaming responses
By default, what a handler returns is serialized in full and sent with Content-Length. For large files or data generated gradually, Corvis can send the body in chunks.
StreamableFile
StreamableFile represents a file to download. Its headers (Content-Type, Content-Disposition, Content-Length) are only written if the response does not already have them, so a #[header(...)] on the route takes precedence.
#[routes]
impl ReportsController {
/// From memory.
#[get(":id/csv")]
fn csv(&self, #[param("id")] id: String) -> StreamableFile {
let bytes = self.reports.render_csv(&id);
StreamableFile::new(bytes).content_type("text/csv").filename(format!("{id}.csv"))
}
/// From disk, in chunks of up to 64 KiB.
#[get(":id/pdf")]
async fn pdf(&self, #[param("id", ParseUUIDPipe)] id: String) -> HttpResult<StreamableFile> {
let file = StreamableFile::from_path(format!("./reports/{id}.pdf"))
.await
.map_err(|_| NotFoundException::new("Report not found"))?;
Ok(file.content_type("application/pdf"))
}
}Ways to build it:
| Constructor | Data source |
|---|---|
StreamableFile::new(bytes) | In-memory data (Vec<u8>, Bytes, String...) |
StreamableFile::with_options(bytes, StreamableFileOptions::new()...) | Same, with options |
StreamableFile::from_stream(stream, options) | A ByteStream |
StreamableFile::from_path(path).await? | A file, read in chunks; the type is deduced from the extension and the length from the size |
And its options, as methods of StreamableFile or StreamableFileOptions:
.content_type(..)/StreamableFileOptions::r#type(..):Content-Type(application/octet-streamby default)..disposition(..): literalContent-Disposition..filename(name): buildsContent-Disposition: attachmentlike Express'sres.attachment()(uses only the base name, escapes quotes and addsfilename*if the name is not latin1)..length(n):Content-Lengthfor a stream; without it, the stream is sent withTransfer-Encoding: chunked.
A custom stream
Response::stream sends any ByteStream, which is a Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>:
use corvis::bytes::Bytes;
use corvis::futures::stream;
#[get("numbers")]
fn numbers(&self) -> Response {
let chunks = stream::iter((1..=1000).map(|n| Ok(Bytes::from(format!("{n}\n")))));
Response::stream(Box::pin(chunks), "text/plain; charset=utf-8")
}Streamed bodies are written as is: without ETag and with the exact Content-Type you specify.
Writing the response by hand with #[res]
With #[res] res: Res the handler owns the response and can finish it later, from another task. It is useful when the result arrives from a callback:
#[get("slow")]
async fn slow(&self, #[res] res: Res) {
corvis::tokio::spawn(async move {
corvis::tokio::time::sleep(std::time::Duration::from_secs(1)).await;
res.json(json!({ "done": true }));
});
}The request stays open until send, json, end, redirect or send_status is called. Res does not offer incremental writes (like res.write in Node.js): to send in chunks, return a stream.
Server-Sent Events
To send events to the browser over an open connection, use #[sse]. See Server-Sent Events.