Laurel decodes form bodies incrementally into caller-owned storage. The form, multipart, upload, and HTTP body layers remain separate so each owner has one bounded state machine and one explicit completion token.
No parser allocates memory, chooses a filesystem path, or owns a request body. Applications provide every result array, byte arena, work buffer, upload store, and HTTP read buffer.
form.UrlEncoded decodes application/x-www-form-urlencoded input across
arbitrary fragment boundaries. A percent escape can span three calls to
form.feed_urlencoded. + becomes a space. Names and values are validated as
strict UTF-8 only after their complete decoded byte sequences are available.
The five limits are independent.
max_bytesbounds encoded request bytes.max_fieldsbounds published fields.max_name_bytesbounds one decoded name.max_value_bytesbounds one decoded value.max_temporary_bytesbounds all retained decoded bytes.
form.finish_urlencoded rejects incomplete percent escapes, empty trailing
separators, and incomplete fields. Any failure clears all published fields and
decoded bytes. form.result succeeds only after finalization.
form.BodyParser joins the decoder to one http.core.body.Reader. It preserves
the reader's pending token, settles rejection exactly once, and never presents
a partial form as a successful result.
A form.PENDING operation means the body is not there yet. A handler returns
handler.suspend(operation.token) and is entered again through its resume
callback once the host has done the I/O, where it calls form.complete_body
with the settled token and carries on. It never spins on the read, so a body
larger than any buffer costs no more than the parser's own storage. See
doc/middleware.md for the suspension contract.
A decoded form.Form keeps every field in wire order. A repeated key keeps
every value, and a field with no = or nothing after it has an empty value,
which is different from an absent field.
form.find(values, name)returns the first field with that exact decoded name, or nil when it is absent.form.find_next(values, field)returns the next field with the same name, so a loop fromfindvisits every value of a repeated key in order.form.typed(field, decoder, allocator)runs one field's value through arouter.Decoder, the same decoders that type path parameters. It returnsokwithnonefor an absent field,okwith the decodedrouter.Value, or the decoder's error when it refuses the value.
query.decode(decoder, storage, limits, raw) decodes the bytes after ? with
the same form.UrlEncoded decoder, the same limits and the same result. There
is one decoder: percent escapes, + as a space, strict UTF-8, repeated keys and
empty values behave exactly as they do in a form body, and form.find,
form.find_next and form.typed read the result. query.parse(context, ...)
decodes the query of the request a handler is serving. An empty or absent
query is a complete result with no fields.
The decoder is initialized with form.SOURCE_QUERY through form.init_source,
which only changes what a client is told. A bad percent escape, a truncated
escape, invalid UTF-8, a null byte, an empty name, an empty field such as &&
and a trailing & are form.MALFORMED with status 400 and malformed query.
A query past any of the limits is form.LIMIT with status 400 and query is too large. It is a bad request rather than 413, since a query is part of the
request target and not content, and the error kinds map 413 to content only. A decoder refused for its storage is form.INVALID with status 500.
A failure publishes no fields, and the same decoder can decode the next query.
raw.Body reads a whole request body into one caller buffer and returns the
exact bytes, for work such as checking a webhook signature over what was sent.
Nothing is decoded. raw.init(body, reader, buffer, limit) takes a reader no
one has read from, so the bytes start at the first byte of the body, and a
buffer of at least limit bytes. The buffer, the reader and the raw.Body
must not overlap.
raw.poll reads until the body ends, fails or waits on the host. It never
returns a partial result:
raw.PENDINGnames the token to wait on. The handler returnshandler.suspend(operation.token)and callsraw.completewith that token when it resumes.raw.COMPLETEcarriesbytes, a view of the whole body in the buffer.raw.LIMITmeans the body is longer thanlimit, with status 413 andrequest body is too large. A declared length over the limit is refused before any byte is read. A body without a declared length is read up to the limit and then one byte further, so a body of exactlylimitbytes fits and one byte more does not. A host body limit reached first is the same failure. The body is refused through its reader, which may itself wait on the host.raw.MALFORMEDmeans a body with a declared length ended short of it or ran past it, with status 400 andmalformed request body.raw.CANCELLEDmeans the request was cancelled or timed out, with 499.raw.FAILEDmeans the transport failed in any other way, including a chunked body cut off mid-stream, with status 500.
Every failure zeroes what was read, so the buffer never holds part of a body.
The reader sees the same http.core.body contract for HTTP/1.1 with a
content-length or chunked coding, and for HTTP/2 and HTTP/3 with or without
a content-length, so raw.Body behaves the same on each.
multipart.Parser recognizes exact CRLF MIME boundaries across arbitrary input
fragments. It supports nested multipart bodies up to MAX_NESTING, quoted
parameters, UTF-8 filename* parameters, fields, empty files, and streamed
files. Preamble bytes are rejected. Epilogue bytes are ignored after the final
top-level boundary.
The multipart limits are independent.
max_total_bytesbounds encoded body bytes.max_partsbounds all field, file, and container parts.max_headers_per_partbounds header count for one part.max_header_bytesbounds retained headers for one part.max_field_bytesbounds one decoded field value.max_file_bytesbounds one streamed file.max_temporary_bytesbounds retained names, values, metadata, and copied file identifiers.max_nestingbounds active multipart containers.
The caller supplies a multipart.Storage whose capacities satisfy the selected
limits. Parser state, upload state, parts, arena bytes, work bytes, provider
state, input fragments, and body-reader buffers must be physically disjoint.
Initialization rejects overlap before any parsing or provider operation.
multipart.feed reports exactly how many input bytes it consumed. When storage
returns pending work, the caller must retain the complete input range and keep
it immutable until multipart.complete_pending settles that token. A fresh
public token is issued for every pending provider turn. Stale tokens never reach
the provider.
multipart.finish is required after the body owner reports end of input. A
closing MIME boundary alone does not publish uploads. Finalization verifies the
top-level close and commits the request upload batch.
multipart.BodyParser joins parsing to one http.core.body.Reader. It can span
pending reads, pending upload operations, and pending body rejection without
losing which owner issued the token.
One upload.Upload represents one request-wide batch. Every file is staged
inside that batch. A file boundary seals only that file. It does not make any
file externally visible. multipart.finish asks the provider to publish every
staged file atomically after the complete body has been accepted.
The state sequence is:
ready
-> batch begin
-> file begin -> write* -> file seal
-> file begin -> write* -> file seal
-> batch commit
-> release
A malformed later part, limit failure, body failure, or cancellation takes the batch abort path. The provider removes every staged file, including files sealed before the failure. Laurel clears every logical part and result byte before it returns the terminal failure.
The upload provider surface has seven operations.
| Callback | Required effect |
|---|---|
batch_begin |
Create an isolated unpublished staging namespace and return its transaction. |
file_begin |
Create one staged file inside the named batch and bind its metadata. |
write |
Consume a nonzero prefix of the supplied bytes for the named batch and file. |
file_seal |
Close one staged file and return its opaque identifier without publishing the batch. |
batch_commit |
Publish all sealed files atomically and exactly once. |
batch_abort |
Remove all staged files and the optional active file exactly once. |
complete |
Continue the one provider operation named by its provider token. |
batch_commit is legal only with at least one sealed file. batch_abort accepts
either an idle batch or a batch with one active file. File-begin failures and
write or seal failures leave the batch active so Laurel can execute its single
abort path.
Each successful batch or file begin returns a Transaction whose owner equals
Store.ctx and whose nonzero identifier is distinct from the other active
transaction. Transaction values are immutable capabilities for their active
operation sequence.
Each successful file seal returns a nonempty provider-owned StoredFile.id and
the exact number of bytes accepted by write. The identifier remains immutable
through the next call on that Upload. The multipart parser copies it into its
caller-owned arena before another provider call, so multipart results never
borrow provider memory.
Store.context_size declares the complete contiguous range beginning at
Store.ctx. Store.aliases_state reports any additional provider-owned ranges.
The ownership query is read-only. It cannot mutate the upload, parser, supplied
range, store descriptor, or completion state.
The Store descriptor is copied into Upload and remains fixed for the whole
session. Every callback is checked against a complete snapshot of upload state.
A callback that mutates Laurel-owned state poisons the batch and requires
external reconciliation.
Metadata views passed to file_begin point into Upload storage. They remain
immutable until synchronous return or final completion of that file-begin
operation. A write input remains borrowed and immutable until synchronous
return or final completion of that write. Providers cannot retain either range
after its operation settles.
One Upload permits one provider operation at a time. A Store can serve many
independent uploads concurrently. The provider owns any synchronization needed
for shared backing storage. Laurel never serializes unrelated requests through
global state.
A pending Progress contains only its nonzero provider token, operation kind,
active outcome, and nil error. All other output fields are empty. A completion
can return another pending provider token. Laurel replaces the public token on
every turn while retaining the exact metadata or input ownership required by
the operation.
Cancellation waits for batch begin, file begin, write, or file seal to settle, then aborts the batch. Cancellation cannot reverse a submitted batch commit. Callers must settle that commit and use its outcome.
A provider reports OUTCOME_UNKNOWN only when it cannot prove whether a finish
operation took effect. Laurel then rejects release, retry, and further parsing.
The application must reconcile against durable provider state.
upload.resolve_unknown accepts OUTCOME_COMMITTED only for an uncertain batch
commit. It accepts OUTCOME_ABORTED for any poisoned batch after the provider
has removed or proven absence of all staged state.
multipart.resolve_storage applies the same resolution to a parser. A committed
resolution publishes the already validated caller-owned result. An aborted
resolution clears all parts and returns the original parser failure, or a
storage failure when no earlier parser failure exists.
No timeout or retry guesses an unknown outcome.