Volga
Home
API Docs
GitHub
  • English
  • Русский
Home
API Docs
GitHub
  • English
  • Русский
  • Home
  • Getting Started

    • Quick Start
    • Route Parameters
    • Query Parameters
    • Route Groups
  • Requests & Responses

    • Headers
    • Handling JSON
    • Handling Form Data
    • Working with Files
    • Multipart Responses
    • Cookies
  • Middleware & Infrastructure

    • Basic Middleware
    • Custom Middleware
    • Parameterized Middleware
    • Response Compression
    • Request Decompression
    • CORS (Cross-Origin Resource Sharing)
    • Static Files
    • Rate Limiting
    • Configuration Files
  • Security & Access

    • Authentication and Authorization
  • Reliability & Observability

    • Global Error Handling
    • Tracing & Logging
    • Request cancellation
  • Protocols & Realtime

    • HTTP/1 and HTTP/2
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Advanced Patterns

    • Dependency Injection
    • Custom Handling of HEAD, OPTIONS, and TRACE Methods

Multipart Responses

Starting from 0.9.2, Multipart in Volga is bidirectional: in addition to acting as a request extractor (see Working with Files), it implements IntoResponse and can be returned from handlers to produce a multipart/* response.

This is useful for:

  • Returning multiple related blobs in a single response (form-data style).
  • Serving partial content for HTTP Range requests as multipart/byteranges.
  • Returning a heterogeneous bundle of parts as multipart/mixed.
  • Proxying or forwarding an incoming multipart back to a client.

Like the request side, multipart responses are gated by the multipart feature. If you're not using the full feature set, enable it explicitly in your Cargo.toml:

[dependencies]
volga = { version = "...", features = ["multipart"] }

Returning a Multipart Response

The simplest way to build an outgoing multipart is Multipart::from_parts, which accepts any IntoIterator<Item = Part>:

use volga::{App, Multipart, multipart::Part};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // GET /form
    app.map_get("/form", || async {
        Multipart::from_parts([
            Part::text("greeting", "hello"),
            Part::text("name", "world"),
        ])
    });

    app.run().await
}

The response gets a Content-Type: multipart/form-data; boundary=... header with an auto-generated boundary, and each Part is encoded with its own Content-Disposition and (where applicable) Content-Type headers.

Building Parts

Part provides a small builder API for the common cases:

MethodUse it for
Part::text(name, value)A simple text/plain; charset=utf-8 field.
Part::bytes(name, bytes)A binary field with application/octet-stream.
Part::file(name, filename, bytes)An in-memory file. Content-Type is auto-inferred from the filename via mime_guess.
Part::stream(name, filename, ct, stream)A streaming-body file part — the body is sent lazily, chunk by chunk.
Part::new(body)A bare part with no Content-Disposition; use the with_* builders to attach headers.

Each builder has a fallible try_* counterpart (try_text, try_bytes, try_file, try_stream, try_with_disposition) — the static-input constructors panic on invalid header bytes, and the try_* variants should be preferred when the name or filename comes from untrusted input.

A typical mixed example combining a text field and an in-memory file:

use bytes::Bytes;
use volga::{App, Multipart, multipart::Part};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_get("/report", || async {
        Multipart::from_parts([
            Part::text("greeting", "hello"),
            Part::file("logo", "logo.bin", Bytes::from_static(b"\x01\x02\x03")),
        ])
    });

    app.run().await
}

Streaming Parts

When a part's body is large or produced incrementally, use Part::stream to send it without buffering. The body must be a Stream<Item = Result<Bytes, volga::error::Error>>:

use bytes::Bytes;
use futures_util::{StreamExt, stream};
use volga::{App, Multipart, multipart::Part};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_get("/stream", || async {
        let chunks = stream::iter([
            Bytes::from_static(b"alpha-"),
            Bytes::from_static(b"beta-"),
            Bytes::from_static(b"gamma"),
        ])
        .map(Ok::<_, volga::error::Error>);

        let part = Part::stream(
            "log",
            "log.txt",
            volga::headers::ContentType::text_utf_8(),
            chunks,
        );
        Multipart::from_parts([part])
    });

    app.run().await
}

If the parts themselves are produced lazily (e.g. enumerating files, computing byte ranges on demand), use Multipart::from_stream — it accepts any Stream<Item = Part> and emits each part as the stream yields it.

Choosing a Subtype

By default, outgoing multiparts use the multipart/form-data subtype. To switch to mixed, byteranges, or any other RFC 2046 subtype, call Multipart::with_subtype:

use bytes::Bytes;
use volga::{App, Multipart, multipart::{MultipartSubtype, Part}};
use volga::headers::{ContentType, HeaderName, HeaderValue};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_get("/ranges", || async {
        let part1 = Part::new(b"first" as &[u8])
            .with_content_type(ContentType::text_utf_8())
            .with_header_raw(
                HeaderName::from_static("content-range"),
                HeaderValue::from_static("bytes 0-4/10"),
            );
        let part2 = Part::new(b"five!" as &[u8])
            .with_content_type(ContentType::text_utf_8())
            .with_header_raw(
                HeaderName::from_static("content-range"),
                HeaderValue::from_static("bytes 5-9/10"),
            );

        Multipart::from_parts([part1, part2])
            .with_subtype(MultipartSubtype::ByteRanges)
    });

    app.run().await
}

The supported variants are:

  • MultipartSubtype::FormData — the default; canonical form / file upload subtype.
  • MultipartSubtype::Mixed — heterogeneous parts.
  • MultipartSubtype::ByteRanges — partial-content responses for HTTP Range requests.
  • MultipartSubtype::Custom(s) — any other subtype, e.g. alternative, related.

Customizing the Boundary

The boundary is generated automatically and is RFC 2046 §5.1.1 compliant. To pin it (useful in tests or when interoperating with a strict client), use Multipart::with_boundary. It validates the input and returns an error if the boundary is malformed:

use volga::{App, Multipart, multipart::Part};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.map_get("/fixed", || async {
        Multipart::from_parts([Part::text("k", "v")])
            .with_boundary("MY-FIXED-BOUNDARY")
    });

    app.run().await
}

Forwarding an Incoming Multipart

When you need to proxy or forward an incoming multipart body back to a client, use Multipart::into_outgoing. It re-encodes the request multipart as a streaming outgoing one — each field becomes a Part with a streaming body:

use volga::{App, Multipart};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // POST /echo — re-emits the incoming multipart back to the caller
    app.map_post("/echo", |multipart: Multipart| async move {
        multipart.into_outgoing()
    });

    app.run().await
}

Note: into_outgoing is not byte-perfect — the boundary is regenerated and header ordering may differ. For byte-perfect passthrough, skip the Multipart extractor and forward the raw HttpBody.

Volga also accepts any multipart/* subtype on the request side (not only multipart/form-data), so forwarding multipart/byteranges, multipart/mixed, etc. works out of the box.

OpenAPI

If you're using OpenAPI integration, OpenApiRouteConfig::produces_multipart(status) describes a multipart/form-data response for a given status code in the generated spec.

A robust runnable example is available here.

Last Updated: 5/6/26, 10:09 AM
Prev
Working with Files
Next
Cookies