multipart

package
v0.26.39 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package multipart — boundary recovery and validation.

The delimiter that separates the parts of a multipart body is announced in the message's Content-Type header, NOT in the body. codec.Codec carries no header, so this file is where the gap is closed: the boundary is re-emitted on every delimiter line, which makes a well-formed body self-describing, and these helpers read it back out. See CLAUDE.md §The boundary problem for what that buys and what it does not.

Package multipart implements RFC 7578 multipart/form-data as a codec.StreamingCodec registered under Format("multipart"). Blank-importing this package is enough to make it resolvable via the core/data/codec registry.

The format is a CONTAINER, not a value serialisation: its native Go shape is FormValue, a list of named PartValue sections. Any other value takes the JSON-mediated shape — a single part named JSONPartName carrying encoding/json's output — so the universal Marshal(F, v) / Unmarshal(F, b, &v) contract holds without a facade-side promotion rule. baseenc is the in-tree precedent for a JSON-mediated pipeline.

Streaming is the point of the format, so codec.StreamingCodec is the interface that matters here: NewEncoder writes one part per Encode straight to the io.Writer, NewDecoder reads one part per Decode straight off the io.Reader, and neither holds more than a single part body at a time. Marshal and Unmarshal are the same two paths driven over a scratch buffer.

The delimiter lives in the message's Content-Type header, which the Codec contract cannot carry. See CLAUDE.md §The boundary problem for how that gap is closed (ContentType on the write side, Boundary on the read side) and for the one case it is NOT closed (a body with a "--"-prefixed preamble, which only the real header can disambiguate — hence BoundaryCodec).

Package multipart — the running tally a LimitsConfig is checked against.

Package multipart — adapts mime/multipart.Reader to codec.Decoder.

This is the ONLY read path in the package: Unmarshal drives this decoder over a bytes.Reader, so a body parsed from memory and a body streamed off a socket go through identical code and identical bounds.

Package multipart — reading a part's Content-Disposition, and the one rule its filename is reduced by on every operating system.

Package multipart — adapts mime/multipart.Writer to codec.Encoder.

This is the ONLY write path in the package: Marshal and Append both drive this encoder over a scratch buffer, so a body built in memory and a body streamed to a socket go through identical code and identical bounds.

Package multipart — the two value types that model a body: one section (PartValue) and the container that frames them (FormValue). They live in one file because neither is meaningful without the other.

Package multipart — the memory bound applied to every decode AND every encode. A multipart body is the shape an upload arrives in, so the decoder is the SDK surface most exposed to a hostile stream: unbounded, a single crafted part drives io.ReadAll to an OOM, and a boundary flood does the same with a very large number of empty parts. Three bounds close both doors (per-part bytes, part count, aggregate bytes); the encoder enforces the identical set so the codec never emits a body it would refuse to read back.

Package multipart — compile-time proof that the concrete types satisfy the contracts this package advertises. Kept in its own *_compliance.go file so the assertions live where a reader looks for them and nowhere else.

Index

Constants

View Source
const DefaultMaxPartBytes int64 = 32 << 20 // 32 MiB

DefaultMaxPartBytes caps one part body at 32 MiB. Generous for a form field or a document upload, and small enough that a single part cannot exhaust a container's memory budget. Callers moving larger objects raise the bound explicitly through NewWithLimits.

View Source
const DefaultMaxParts int = 1024

DefaultMaxParts caps a body at 1024 parts. A boundary flood costs the attacker one header line per part while costing the decoder a header parse and a slice entry each, so the part COUNT needs its own ceiling — the byte bounds alone do not constrain it on a streaming reader.

View Source
const DefaultMaxTotalBytes int64 = 64 << 20 // 64 MiB

DefaultMaxTotalBytes caps the sum of every part body at 64 MiB. Without it the real ceiling would be DefaultMaxPartBytes × DefaultMaxParts, which is not a bound anybody chose.

View Source
const JSONPartName string = "_json"

JSONPartName is the field name of the part the JSON-mediated shape writes and reads. It matches pkg/v1/data/codec's csv promotion header for the same reason: one recognisable label for "the SDK put a JSON document here".

Variables

View Source
var (
	//: register the singleton and expose it as a typed package var. The
	//: registered instance carries the package defaults; a caller needing
	//: other bounds builds an unregistered one with NewWithLimits.
	Codec codec.Codec = codec.Register(&multipartCodec{limits: defaultLimits()})
)

The registered singleton. Its aliases are literals in MIMETypes and Extensions, not package tables: Register reads them through the Codec interface, which orders no table before this initialiser (see core/data/codec.Register).

Functions

func Boundary

func Boundary(data []byte) (boundary string, err error)

Boundary recovers the RFC 2046 delimiter from a multipart body by reading it back off the body's first delimiter line.

This is EXACT for every body this package produced and for every RFC 7578 form-data body in the wild, because form-data producers emit no preamble. It is a recovery, not an oracle: a body whose preamble contains a line that both starts with "--" and parses as a valid boundary would hand back the wrong answer. When the real Content-Type header is available, pass it to NewDecoderWithBoundary instead of relying on this.

func ContentType

func ContentType(data []byte) (value string, err error)

ContentType returns the full Content-Type header value for a body this package produced — "multipart/form-data; boundary=…", quoted by mime.FormatMediaType when the boundary needs it.

Marshal cannot set a header, so this is how the caller obtains the one piece of the message the Codec contract has nowhere to put.

func New

func New() codec.Codec

New returns the multipart codec singleton, bounded by the package defaults.

func NewWithLimits

func NewWithLimits(l LimitsConfig) (c codec.Codec, err error)

NewWithLimits returns an unregistered codec bounded by l. Zero fields take the package defaults; a negative field is refused with LimitsInvalid rather than guessed at (ADR 0031).

The instance is deliberately NOT added to the registry: Format("multipart") already resolves to the default-bounded singleton, and a second registration under the same name would panic at boot. csv.NewWithEscape is the precedent.

Types

type BoundaryCodec

type BoundaryCodec interface {
	codec.Codec
	// NewEncoderWithBoundary streams a body framed by the given boundary.
	NewEncoderWithBoundary(w io.Writer, boundary string) (enc codec.Encoder, err error)
	// NewDecoderWithBoundary streams a body framed by the given boundary,
	// bypassing recovery from the wire entirely.
	NewDecoderWithBoundary(r io.Reader, boundary string) (dec codec.Decoder, err error)
}

BoundaryCodec is the optional extension this package implements on top of codec.StreamingCodec: the two constructors that take the boundary from the caller instead of recovering it from the bytes.

This is the honest form of the streaming API. An HTTP server already holds the authoritative boundary in the request's Content-Type header, and an HTTP client must set that header before it writes the first body byte — neither can be expressed through NewEncoder / NewDecoder, whose signatures the domain fixes. Callers reach it with a type assertion, the same way they reach codec.Appender.

type BoundaryProvider

type BoundaryProvider interface {
	// Boundary returns the delimiter the encoder writes.
	Boundary() string
}

BoundaryProvider is implemented by the codec.Encoder this package returns, so a caller streaming a request body can read the generated delimiter and set its Content-Type header before the first byte goes out.

type FormValue

type FormValue struct {
	// Boundary is the RFC 2046 delimiter framing the parts.
	Boundary string
	// Parts are the body's sections, in wire order.
	Parts []PartValue
}

FormValue is a whole multipart/form-data body: its RFC 2046 delimiter and its sections, in wire order.

Boundary is the one field that has no equivalent in the codec.Codec contract, which carries no Content-Type. Empty on Marshal means "generate one"; Unmarshal always fills it with the delimiter it recovered, so a decode → encode round trip keeps the original delimiter. It reproduces the whole body byte for byte only for a body this package encoded, and whose filenames carry no path separator: a PartValue models a part's name, filename, media type and bytes, so any other part header — a Content-Transfer-Encoding, a custom one — and another producer's header order or casing are not carried through, and a filename is decoded to its last path element (see PartValue.FileName).

type LimitsConfig

type LimitsConfig struct {
	// MaxPartBytes caps a single part body; 0 selects DefaultMaxPartBytes.
	MaxPartBytes int64
	// MaxParts caps the number of parts; 0 selects DefaultMaxParts.
	MaxParts int
	// MaxTotalBytes caps the sum of part bodies; 0 selects DefaultMaxTotalBytes.
	MaxTotalBytes int64
}

LimitsConfig bounds what a multipart body may materialise. Every field is a ceiling, never a target.

A ZERO field means "use this package's documented default" — never "unlimited" (ADR 0031: a policy's zero value is a safe default or an explicit refusal, never an inert policy). A NEGATIVE field is refused at construction with LimitsInvalid: the SDK can supply a ceiling the caller forgot, but it cannot tell a typo apart from a request for no ceiling at all, and guessing would silently remove the guard the type exists to place.

There is deliberately no way to spell "unlimited". A decoder without a ceiling is the defect this type prevents.

type PartValue

type PartValue struct {
	// Name is the form field name (Content-Disposition name=). Required: Encode
	// refuses a part without one, and decoding refuses a body carrying one.
	Name string
	// FileName is the optional filename= parameter; empty for a plain field.
	// UTF-8 is written as-is (RFC 7578 §4.2); only CR, LF and NUL are refused.
	//
	// Decoding keeps only its LAST PATH ELEMENT, and reads both '/' and '\' as
	// separators on every operating system: RFC 7578 §4.2 has the receiver drop
	// the directory information a sender may include, and the sender writes it
	// with its own separator. `C:\Users\ada\cv.pdf` and `dir/cv.pdf` both
	// decode to `cv.pdf`, wherever the decoder runs. Nothing else is removed —
	// "." and ".." come back as sent — so a caller turning it into a path still
	// validates it first.
	FileName string
	// ContentType is the optional per-part media type; empty omits the header.
	ContentType string
	// Data is the part body.
	Data []byte
}

PartValue is one section of a multipart/form-data body: a named field, optionally a filename and a media type, and the bytes themselves.

The body is materialised. Streaming happens BETWEEN parts (one part per Encode / Decode), not inside one: codec.Decoder's Decode(v any) has nowhere to hand back an io.Reader the caller must drain before the next call, so a part body is read whole under LimitsConfig.MaxPartBytes. A consumer moving objects too large for that ceiling raises it with NewWithLimits, or reaches for mime/multipart directly — see CLAUDE.md §Not covered.

Name, FileName and ContentType are written into the part's header block, so a CR, an LF or a NUL in any of them is refused with ValueInvalid naming the field: a line break there would end the header line and let the rest of the value write headers of its own. Nothing is escaped or repaired on the way.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL