← Back to Documentation

Request Bodies

Bodies that stream through your feature set cost nothing. Bodies you actually read become values — and values have a size limit.

Streams don't have a size. Values do.

A route that only moves its request body never builds it in memory. Write it to a file, Send it on, Return it, iterate it with for each — the body's size is bounded by the sink, and no limit applies. A four-gigabyte upload is fine:

(uploadDocument: Files API) {
    Extract the <upload> from the <request: body>.   (* binds — reads nothing *)
    Write the <upload> to the <file: "./uploads/doc.bin">.   (* streams *)
    Return a <Created: status> with <upload>.
}

A route that reads its body turns it into a value, and that is what is bounded. A field access, a Compute, a Store, a Log, or a when guard all count as reading:

(createNote: Files API) {
    Extract the <body> from the <request: body>.
    Extract the <title> from the <body: title>.   (* reads — now it is a value *)
    Store the <note> into the <note-repository> with <body>.
    Return a <Created: status> with <title>.
}

The limit

The default is 1 MB. A route raises or lowers it in the contract, and the route's own declaration always wins:

paths:
  /notes:
    post:
      operationId: createNote
      x-aro-max-body: 256KB

Change the default for routes that declare nothing, from ARO or the environment:

Configure the <http-server: max-body> with "1MB".
ARO_MAX_BODY=512KB aro run ./MyApp

A body over the limit is answered 413 before it is read — from Content-Length where the client declared one, incrementally otherwise.

aro check tells you which kind each route is

Which of the two a route is gets computed from the source before the server binds a port, so you find out at check time rather than in production:

$ aro check ./FilesAPI

Request bodies:
  streams POST /docs — no limit applies, nothing is buffered
      note: x-aro-max-body is unused here; this route never builds the body
  holds  POST /notes — up to 256KB in memory (Extract the <title> from the <body: title>, line 14)

Anything the analysis cannot see through — a plugin action, an unknown verb — counts as reading. Erring toward the bounded answer is the safe direction.

One arrival, one consumption

A body arrives once and can be consumed once. Emit or Publish of a body anchors it: it is drained to a temporary file a chunk at a time, so handlers that outlive the request can each read it, and the file is deleted when the last reference goes.

Folding qualifiers consume the body chunk by chunk, so hashing a 4 GB upload costs a chunk of memory too — and it hashes the upload's bytes, not a rendering of its parsed form:

Extract the <upload> from the <request: body>.
Compute the <digest: sha256> from <upload>.
Compute the <size: length> from <upload>.

aro run and aro build agree

The analysis is baked into the binary at build time, so a compiled server enforces the same limits and streams the same bodies as the interpreter.

Learn More

The full specification is ARO-0090: Streaming I/O and Materialization Limits, and Examples/FileUpload is a runnable application. For how the same machinery pipelines collections, see Streaming Execution and Concurrency.