Skip to content

Multipart Forms — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Multipart Forms. We cover key concepts, practical examples, and best practices to help you master this topic.

Multipart/form-data encoding splits an HTTP request body into multiple parts separated by a unique boundary string, each part carrying its own headers and binary content.

What You'll Learn

By the end of this lesson, you will understand how multipart encoding structures file uploads, how to parse multipart requests, and how to stream large files without buffering everything in memory.

Why It Matters

Every file upload on the web uses multipart encoding. Understanding how it works helps you debug upload issues, choose the right Parsing library, and optimize memory usage for large files.

Real-World Use

A medical imaging platform receives DICOM files and patient metadata in a single multipart request. The server must parse the form fields and the binary image data, validate each part, and store them separately.

Multipart Structure

flowchart TB
    Request[HTTP Request Body] --> Boundary[Boundary Delimiter]
    Boundary --> Part1[Part 1: Text Field]
    Boundary --> Part2[Part 2: File Field]
    Boundary --> Part3[Part 3: Second File]
    Part1 --> H1[Headers: Content-Disposition]
    Part2 --> H2[Headers: Content-Disposition + Content-Type]
    Part3 --> H3[Headers + Binary Data]

Understanding the Boundary

The boundary is a unique string that separates parts. The client generates it and includes it in the Content-Type header. Each part starts with --boundary and the body ends with --boundary--.

# multipart_parser.py
# Simulating a multipart request structure
import uuid
from typing import List, Dict, Tuple

class MultipartPart:
    def __init__(self, headers: Dict[str, str], body: bytes):
        self.headers = headers
        self.body = body
        self.name = self._parse_name()
        self.filename = self._parse_filename()

    def _parse_name(self):
        disp = self.headers.get("Content-Disposition", "")
        for part in disp.split(";"):
            part = part.strip()
            if part.startswith("name="):
                return part[5:].strip('"')
        return ""

    def _parse_filename(self):
        disp = self.headers.get("Content-Disposition", "")
        for part in disp.split(";"):
            part = part.strip()
            if part.startswith("filename="):
                return part[9:].strip('"')
        return None

    def __repr__(self):
        return f"Part(name={self.name}, filename={self.filename}, size={len(self.body)})"

class MultipartRequest:
    def __init__(self, boundary: str):
        self.boundary = boundary
        self.parts: List[MultipartPart] = []

    def parse(self, body: bytes):
        raw_boundary = f"--{self.boundary}".encode()
        closing_boundary = f"--{self.boundary}--".encode()

        raw_parts = body.split(raw_boundary)
        for raw in raw_parts:
            raw = raw.strip(b"\r\n")
            if not raw or raw == closing_boundary.strip(b"--"):
                continue
            if raw.startswith(b"Content-Disposition"):
                header_end = raw.find(b"\r\n\r\n")
                header_section = raw[:header_end].decode()
                body_section = raw[header_end + 4:]

                headers = {}
                for line in header_section.split("\r\n"):
                    if ":" in line:
                        key, val = line.split(":", 1)
                        headers[key.strip()] = val.strip()

                self.parts.append(MultipartPart(headers, body_section))

boundary = "----FormBoundary" + uuid.uuid4().hex[:8]
req = MultipartRequest(boundary)

body_parts = []
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="username"\r\n\r\n')
body_parts.append(b"alice\r\n")
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="avatar"; filename="photo.jpg"\r\n')
body_parts.append(b"Content-Type: image/jpeg\r\n\r\n")
body_parts.append(b"\xff\xd8\xff\xe0" + b"fakejpegdata" * 10 + b"\r\n")
body_parts.append(f"--{boundary}--\r\n".encode())

req.parse(b"".join(body_parts))
for p in req.parts:
    print(p)

Expected output:

Part(name=username, filename=None, size=5)
Part(name=avatar, filename=photo.jpg, size=123)

Fields vs Files

In a multipart form, regular fields and file fields are structurally identical. The difference is that a file part includes a filename parameter in its Content-Disposition header and a Content-Type header. A text field has neither.

# field_vs_file.py
from typing import Dict

def classify_part(headers: Dict[str, str], body: bytes) -> str:
    disp = headers.get("Content-Disposition", "")
    content_type = headers.get("Content-Type", "")
    has_filename = "filename=" in disp
    is_text = content_type.startswith("text/") or not content_type

    if has_filename:
        return "file"
    elif is_text and len(body) < 1024:
        return "text_field"
    else:
        return "unknown"

samples = [
    ({"Content-Disposition": 'form-data; name="email"'}, b"a@b.com"),
    ({"Content-Disposition": 'form-data; name="file"; filename="doc.pdf"',
      "Content-Type": "application/pdf"}, b"%PDF-1.4"),
    ({"Content-Disposition": 'form-data; name="desc"',
      "Content-Type": "text/plain"}, b"hello world"),
]

for headers, body in samples:
    print(f"  {classify_part(headers, body)}")

Expected output:

  text_field
  file
  text_field

Streaming Multipart Parsing

Reading the entire request body into memory does not scale for large files. Streaming parsers Process parts as they arrive, writing files to disk without buffering the whole body.

# streaming_multipart.py
# Concept: processing parts as boundaries are encountered
import io
from typing import Callable

class StreamingMultipartParser:
    def __init__(self, boundary: str, on_part: Callable):
        self.boundary = boundary.encode()
        self.on_part = on_part
        self.buffer = b""
        self.current_headers = {}
        self.in_headers = True

    def feed(self, chunk: bytes):
        self.buffer += chunk
        if self.in_headers:
            self._parse_headers()

    def _parse_headers(self):
        end = self.buffer.find(b"\r\n\r\n")
        if end == -1:
            return
        header_bytes = self.buffer[:end]
        for line in header_bytes.decode().split("\r\n"):
            if ":" in line:
                k, v = line.split(":", 1)
                self.current_headers[k.strip()] = v.strip()
        body_start = end + 4
        self.in_headers = False
        self.buffer = self.buffer[body_start:]

parser = StreamingMultipartParser(
    "----Boundary7MA4YWxkTrZu0gW",
    lambda headers, data: print(f"Received: {headers.get('Content-Disposition', '')[:40]}...")
)

chunk1 = b'------Boundary7MA4YWxkTrZu0gW\r\n'
chunk1 += b'Content-Disposition: form-data; name="file"; filename="test.txt"\r\n\r\n'
chunk1 += b'hello world\r\n'
parser.feed(chunk1)

Expected output:

Received: form-data; name="file"; filename="test.txt"...

Common Mistakes

1. Hardcoding Boundary Values

Each request uses a unique boundary. Never hardcode it; always extract it from the Content-Type header.

2. Not Handling Missing Boundary

If the boundary is missing or malformed, parsing fails. Always validate the Content-Type before parsing.

3. Loading Entire Body Into Memory

For large files, reading the whole body into memory can crash the server. Use streaming parsers.

4. Ignoring Trailing Boundary

The final --boundary-- is the proper end marker. Some parsers stop at the first boundary, leaving unread data in the stream.

5. Confusing Content-Type with File Extension

The Content-Type in a multipart part is advisory. A JPEG file might be declared as text/plain. Always verify actual content.

Practice Questions

1. What character sequence separates parts in a multipart request?

--boundary at the start of each part, --boundary-- at the end.

2. How does the server know a part contains a file?

The Content-Disposition header includes a filename parameter.

3. Why is streaming parsing important for file uploads?

It prevents loading the entire request body into memory, allowing large file uploads without exhausting server RAM.

4. What happens if the boundary value appears inside uploaded file data?

Multipart encoding requires the boundary to be unique. Multipart clients generate random boundaries to avoid collision.

Challenge

Build a multipart parser that writes each file part to a separate temp file while keeping text fields in memory, then returns both when parsing completes.

FAQ

What is a boundary in multipart encoding?

A unique string that separates parts in the request body. It appears in the Content-Type header and as a delimiter between parts.

Can I use a custom boundary value?

The client generates it. The server reads it from the Content-Type header. You should not hardcode it.

What is the overhead of multipart encoding?

Each part adds headers (about 100-300 bytes) plus the boundary delimiters. For large files, this overhead is negligible.

How does multipart handle binary data?

Each part contains raw bytes after its headers. No encoding transformation is applied, so binary data transfers as-is.

Can a multipart form include nested parts?

No. Multipart is flat, not nested. Each part is independent at the top level.

Mini Project: Boundary Generator

# boundary_generator.py
import secrets
import re

class BoundaryGenerator:
    SAFE_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    @classmethod
    def generate(cls, prefix: str = "----FormBoundary") -> str:
        suffix = "".join(secrets.choice(cls.SAFE_CHARS) for _ in range(24))
        boundary = f"{prefix}{suffix}"
        return boundary

    @classmethod
    def extract_from_header(cls, content_type: str) -> str:
        match = re.search(r'boundary=([^;\s]+)', content_type, re.IGNORECASE)
        if not match:
            raise ValueError("No boundary found in Content-Type")
        return match.group(1)

b1 = BoundaryGenerator.generate()
print(f"Boundary: {b1}")

ct = f"multipart/form-data; boundary={b1}"
print(f"Header:   {ct}")

extracted = BoundaryGenerator.extract_from_header(ct)
print(f"Extracted: {extracted}")
print(f"Match: {b1 == extracted}")

Expected output:

Boundary: ----FormBoundarya3Bf8kLm9xQw2Rz7Yp5VnC1j
Header:   multipart/form-data; boundary=----FormBoundarya3Bf8kLm9xQw2Rz7Yp5VnC1j
Extracted: ----FormBoundarya3Bf8kLm9xQw2Rz7Yp5VnC1j
Match: True

What's Next

You understand multipart form encoding. Next, learn how to enforce file size limits at the server level, then explore restricting allowed file types.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro