Streaming bodies
For payloads that are not naturally a single Go value — large file
downloads, log streams, raw binary uploads — runtime.ByteStreamConsumer
and runtime.ByteStreamProducer give you io.Reader / io.Writer
access without the runtime decoding into a typed model.
Server — streaming a download
Spec:
Wiring:
api := untyped.NewAPI(doc).WithJSONDefaults()
// ByteStreamProducer is registered by WithJSONDefaults under
// runtime.DefaultMime ("application/octet-stream"), but be explicit
// when more than one stream-producing MIME is in the picture:
api.RegisterProducer(runtime.DefaultMime, runtime.ByteStreamProducer())
api.RegisterOperation("get", "/backups/{id}", runtime.OperationHandlerFunc(
func(_ any) (any, error) {
f, err := os.Open("/var/backups/2026-05-10.tar")
if err != nil {
return nil, err
}
// The Producer copies whatever io.Reader you return into the
// response writer. Returning *os.File is fine; close it from
// a Responder if you need ownership semantics.
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
defer f.Close()
w.Header().Set("Content-Type", runtime.DefaultMime)
_ = p.Produce(w, f)
}), nil
},
))Full source: docs/examples/contenttypes/streamingbodies/main.go
Produce accepts an io.Reader (yes, despite the name): the
default ByteStreamProducer copies bytes through. For typed bodies
the runtime would marshal first; here you stay in raw-byte territory
end to end.
Server — streaming an upload
Spec:
Wiring:
api.RegisterConsumer(runtime.DefaultMime, runtime.ByteStreamConsumer(
runtime.ClosesStream, // closes the io.ReadCloser when done
))Full source: docs/examples/contenttypes/streamingbodies/main.go
ClosesStream is the option to use when the consumer should
Close() the underlying reader after consumption. Default is not
to close — useful when you want to inspect the same body twice or
the caller manages the lifetime explicitly.
The bound parameter is an io.ReadCloser; stream straight to disk:
api.RegisterOperation("post", "/backups", runtime.OperationHandlerFunc(
func(params any) (any, error) {
body := params.(putBackupParams).Blob // io.ReadCloser
defer body.Close()
f, err := os.CreateTemp("", "upload-*")
if err != nil {
return nil, err
}
defer f.Close()
if _, err := io.Copy(f, body); err != nil {
return nil, err
}
return map[string]string{"status": "ok"}, nil
},
))Full source: docs/examples/contenttypes/streamingbodies/main.go
Client — sending and receiving streams
Build a client request whose body is an io.Reader (or
runtime.NamedReadCloser if you also want a filename for the
Content-Disposition):
file, _ := os.Open("./backup.tar")
defer file.Close()
op := &runtime.ClientOperation{
ID: "PutBackup",
Method: "POST",
PathPattern: "/backups",
ConsumesMediaTypes: []string{runtime.DefaultMime},
Params: putBackupRequest{body: file},
Reader: putBackupResponse{},
}
_, err := rt.SubmitContext(ctx, op)Full source: docs/examples/contenttypes/streamingbodies/main.go
For multipart uploads with file parts and form fields, the shape differs — see client multipart (queued).
Choosing between ByteStream and a typed Consumer
Use ByteStreamConsumer / Producer when:
- the payload is genuinely opaque bytes (downloads, uploads of binary blobs, logs)
- the size could exceed RAM — buffered codecs would OOM
- you want to forward the body to another service without re-encoding
Use a typed Consumer/Producer (JSON, XML, …, custom codec) when the payload is a structured value the operation handler needs to inspect.
The two are not mutually exclusive — a single API can route some
operations to streams and others to typed payloads via
operation-level consumes / produces.