Protocol Reference

Every stdin/stdout line is a JSON object with a code field that identifies its type. Runtime protocol/log events are emitted on stdout only; stderr is not a protocol channel.

In CLI mode (afhttp METHOD URL [flags]), output is the same schema but id and tag fields are omitted. Use --output yaml or --output plain for human-readable output (server response body is never modified). See cli.md for CLI usage.

In pipe mode (afhttp --mode pipe), all fields including id and tag are present.

Input (stdin)

request

FieldRequiredDescription
codeyes"request"
idyesClient-assigned opaque string, echoed in every output for this request. Must be unique across all currently in-flight requests and open WebSocket connections — duplicate id returns error_code: "invalid_request" immediately.
tagnoOpaque string echoed in every output for this request — useful for grouping or correlation. Not interpreted by afhttp.
methodyesGET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
urlyesFull URL including scheme and host
headersnoMerged with defaults.headers_for_any_hosts — request wins on key conflict, null value removes a default
bodynoRequest body — object/array/number/bool → serialized as JSON, sets Content-Type: application/json; string → raw bytes, no implicit Content-Type
body_base64noBase64-encoded binary body — no implicit Content-Type; set explicitly via headers if needed
body_filenoPath to file used as request body — no implicit Content-Type; set explicitly via headers if needed
body_multipartnoMultipart form parts — sets Content-Type: multipart/form-data; boundary=... (see below)
body_urlencodednoURL-encoded form fields — sets Content-Type: application/x-www-form-urlencoded (see below)
optionsnoPer-request options (see below)

body, body_base64, body_file, body_multipart, and body_urlencoded are mutually exclusive. body with object/array/number/bool values automatically sets Content-Type: application/json; all other body types require an explicit Content-Type header. (body_multipart and body_urlencoded always set their respective Content-Type automatically.)

Multipart parts (body_multipart)

Each part: name (required), plus one of value (text string), value_base64 (binary), or file (path). Optional filename and content_type overrides.

URL-encoded fields (body_urlencoded)

Array of {"name": "...", "value": "..."} objects. afhttp percent-encodes both name and value per the application/x-www-form-urlencoded spec: unreserved chars (A-Z a-z 0-9 - _ . *) pass through unchanged; spaces → +; all other bytes → %XX. Duplicate names are supported — use separate array entries.

{"code":"request","id":"1","method":"POST","url":"https://api.example.com/token",
 "body_urlencoded":[
   {"name":"grant_type","value":"authorization_code"},
   {"name":"code","value":"abc123"},
   {"name":"redirect_uri","value":"https://app.example.com/cb"}
 ]}

Options

FieldDefaultDescription
timeout_idle_sconfigNo-data timeout in seconds — abort if no bytes received for this long
retryconfigRetry count for retryable transport errors
response_redirectconfigRedirect limit (0 to disable)
response_parse_jsonconfigParse JSON response body into an object
response_decompressconfigAuto-decompress response body
response_save_resumeconfigResume download if response_save_file file exists — adds Range header, appends on 206
retry_on_statusconfigHTTP status codes that trigger automatic retry (e.g. [429, 503]). Replaces the config list entirely — does not merge.
response_max_bytesHard limit on response body size. Excess returns error_code: "response_too_large".
chunkedfalseDeliver response body in chunks instead of buffering
chunked_delimiter"\n"Split delimiter: "\n" (NDJSON), "\n\n" (SSE), null (raw HTTP chunks, binary data_base64)
response_save_fileSave response body to this path
progress_bytes0Emit progress log every N bytes (file download only, 0=disabled). Works simultaneously with progress_ms.
progress_ms10000Emit progress log every N ms (file download only, 0=disabled). Works simultaneously with progress_bytes.
upgrade"websocket" to open a WebSocket connection
tlsPer-request TLS override — builds a one-off client, no connection pool sharing. Fields: insecure, cacert_pem, cacert_file, cert_pem, cert_file, key_pem_secret, key_file

config

Partial update — only provided fields change. Deep-merged for nested objects (defaults.headers_for_any_hosts, host_defaults). Echoes full config after applying.

Global config fields

FieldDefaultDescription
response_save_dir<system-temp>/afhttp/{uuid}Directory for auto-saved response bodies (uses OS temp dir; e.g. /tmp/... on macOS/Linux, %TEMP%\\... on Windows). Must be writable.
response_save_above_bytes10485760Responses larger than this are auto-saved to response_save_dir/{id} and returned as body_file.
request_concurrency_limit0Max concurrent in-flight requests (0 = unlimited). New requests above the limit return error_code: "overloaded".
timeout_connect_s10TCP+TLS handshake timeout. Triggers client rebuild.
pool_idle_timeout_s90How long an idle connection is kept open (seconds). Triggers client rebuild.
retry_base_delay_ms100Base delay for first retry. Subsequent: base × 2^(attempt-1).
proxynullProxy URL (http://, https://, socks5://). Env vars HTTP_PROXY/HTTPS_PROXY/NO_PROXY take priority. Triggers client rebuild.
tls.insecurefalseSkip certificate and hostname verification. Triggers client rebuild.
tls.cacert_pemnullInline CA certificate (PEM text). Clears cacert_file. Triggers client rebuild.
tls.cacert_filenullPath to CA certificate (PEM) — like curl --cacert. Clears cacert_pem. Triggers client rebuild.
tls.cert_pemnullInline client certificate (PEM text). Clears cert_file. Triggers client rebuild.
tls.cert_filenullPath to client certificate (PEM) for mTLS. Clears cert_pem. Triggers client rebuild.
tls.key_pem_secretnullInline private key (PEM, unencrypted). Redacted in config echo. Clears key_file. Triggers client rebuild.
tls.key_filenullPath to private key (PEM). If absent, key is expected in the same file as cert_file. Clears key_pem_secret. Triggers client rebuild.
log[]Diagnostic event categories to emit as log events: startup, progress, request, retry, redirect

“Triggers client rebuild” — the connection pool is recreated; existing pooled connections are dropped.

TLS settings are not applied to WebSocket connectionswss:// uses the system certificate store. A log event (websocket_tls_config_ignored) is emitted in the request log category when a WebSocket request is made while non-default TLS config is active.

Request defaults (defaults)

Applied to every request; overridable per-request via options.

FieldDefaultDescription
headers_for_any_hosts{"User-Agent":"afhttp/<version>"}Merged into every request. null value removes a default header. Use only for non-sensitive public headers (for example User-Agent, Accept). Do not place credentials or tokens here.
timeout_idle_s30No-data timeout — abort if no bytes received for this many seconds
retry0Retry attempts for retryable transport errors
response_redirect10Maximum redirects to follow (0 to disable)
response_parse_jsontrueParse JSON content-type responses into objects
response_decompresstrueAuto-add Accept-Encoding and decompress. Not applied when Accept-Encoding is set explicitly.
response_save_resumefalseResume interrupted downloads (requires response_save_file)
retry_on_status[]HTTP status codes that trigger automatic retry

Per-host defaults (host_defaults)

{"code":"config","host_defaults":{"api.example.com":{"headers":{"Authorization":"Bearer sk-xxx"}}}}

Merge order for every request: global defaultshost_defaults[host] → per-request headers.

Credential headers (for example Authorization, x-api-key, cookies) must be configured in host_defaults, never in defaults.headers_for_any_hosts.

Other input commands

codeFieldsDescription
sendid, data or data_base64Send a message on an open WebSocket. data: object/array → JSON text frame, string → raw text frame. data_base64 → binary frame. Mutually exclusive.
cancelidCancel an in-flight HTTP request (→ error with cancelled) or close a WebSocket (→ chunk_end).
pingHealth check. Returns pong.
closeGraceful shutdown — cancels in-flight work, waits up to 5s, emits terminal events, then exits.

Output (stdout)

Agent Consumption Contract

For each request id, consume events as a finite state machine:

Response codes

codeDescription
responseBuffered HTTP response (any status, including 4xx/5xx)
errorTransport-level failure — see error codes below
chunk_startChunked/download/WebSocket opened
chunk_dataOne chunk or WebSocket message
chunk_endChunked/download/WebSocket completed
configConfig echo (after config command)
pongReply to ping
closeShutdown acknowledgement
logDiagnostic event — includes startup, progress, request, retry, redirect events

response

FieldPresentDescription
idpipe mode onlyEchoed from request
tagif setEchoed from request
statusalwaysHTTP status code
headersalwaysAll keys lowercase. Single value → string. Multiple values (e.g. Set-Cookie) → array.
bodyif presentJSON content-type + response_parse_json: true → parsed object; text/* with valid UTF-8 → string
body_base64if presentBinary body, or text/*/JSON body with invalid UTF-8 bytes (original bytes preserved exactly, base64-encoded)
body_fileif presentPath where body was saved (exceeded response_save_above_bytes or response_save_file was set)
body_parse_failedif trueContent-Type was application/json but parsing failed. body contains raw text (valid UTF-8), or body_base64 contains original bytes (invalid UTF-8).
tracealwaysSee Trace below

Body selection rules (when response_parse_json: true):

error

FieldDescription
idEchoed from request (absent when input is completely unparseable; absent in CLI mode)
tagEchoed from request if set
error_codeMachine-readable code (see table below)
errorHuman-readable detail
retryableWhether retrying may help
traceSee Trace below

code: "response" with any HTTP status (including 4xx/5xx) is not an error — it means the transport succeeded. code: "error" means the transport itself failed.

Error codes

error_coderetryableCause
dns_failedtrueDNS resolution failed
connect_refusedtrueTCP connection refused or reset
connect_timeouttrueTCP+TLS handshake exceeded timeout_connect_s
tls_errorfalseTLS handshake or certificate verification failure
request_timeoutfalseIdle timeout — no data received within timeout_idle_s
too_many_redirectsfalseExceeded response_redirect
response_too_largefalseBody exceeded response_max_bytes
overloadedtrueRequest rejected because in-flight request limit was reached
chunk_disconnectedfalseConnection lost during chunked/WebSocket/download
cancelledfalseCancelled via cancel command
invalid_requestfalseMalformed JSON, missing field, or duplicate id
invalid_responsefalseServer protocol violation (e.g. non-ASCII header bytes)
internal_errorfalseInternal serialization/output failure (rare)

Retryable errors are automatically retried up to retry with exponential backoff: delay for attempt N = retry_base_delay_ms × 2^(N-1).

chunk_start

FieldDescription
idRequest id (pipe mode only, omitted in CLI mode)
tagEchoed from request if set
statusHTTP status (200 for chunked/download, 101 for WebSocket)
headersResponse headers (same format as response)
content_length_bytesParsed Content-Length, if present

chunk_data

FieldDescription
idRequest id (pipe mode only, omitted in CLI mode)
dataText chunk (valid UTF-8) or WebSocket text frame
data_base64Binary chunk, WebSocket binary frame, or text chunk with invalid UTF-8 bytes (raw mode or binary frame)

chunk_end

FieldDescription
idRequest id (pipe mode only, omitted in CLI mode)
tagEchoed from request if set
body_filePath where body was saved (file download only)
traceSee Trace below

Trace

duration_ms is always present. Other fields are best-effort.

FieldDescription
duration_msTotal wall-clock time including redirects and retries
http_versionh1, h2, or ws
remote_addrServer IP address
sent_bytesRequest body bytes sent
received_bytesResponse body bytes received
redirectsNumber of redirects followed
chunksNumber of chunks or WebSocket messages delivered

pong

{"code":"pong","trace":{"uptime_s":42,"requests_total":100,"connections_active":3}}

log

All diagnostic output uses code: "log" with an event field identifying the category. Emitted only when the category is enabled in config.log. In CLI mode, use --log <categories> or --verbose to enable.

{"code":"log","event":"startup","version":"<version>","argv":["afhttp","--mode","pipe","--log","startup"],"config":{...}}
{"code":"log","event":"progress","id":"dl-1","received_bytes":10485760,"total_bytes":104857600,"percent":10,"eta_s":27}
{"code":"log","event":"request","id":"req-1","implicit_headers":{"Content-Type":"application/json","Accept-Encoding":"gzip, deflate, br"}}
{"code":"log","event":"retry","id":"req-3","host":"api.example.com","reason":"connection_reset","attempt":1,"delay_ms":100}
{"code":"log","event":"redirect","id":"req-5","status":301,"from":"http://example.com/api","to":"https://example.com/api"}

startup fields

FieldDescription
versionafhttp version string
argvProcess arguments as an array
configFull resolved config at startup

progress fields

FieldDescription
idRequest id (pipe mode only)
received_bytesBytes received so far
total_bytesTotal expected bytes (from Content-Length, if known)
percentDownload progress percentage 0–100 (if total_bytes known)
eta_sEstimated seconds remaining (if total_bytes known and download rate measurable)

Progress is emitted during file downloads. Both progress_ms (time-based) and progress_bytes (byte-count-based) triggers work simultaneously — whichever fires first emits a progress event.

request fields

FieldDescription
idRequest id
implicit_headersHeaders that afhttp added automatically (not in the user-supplied headers map). Currently: Content-Type: application/json when body is a JSON value (object/array/number/bool); Content-Type: application/x-www-form-urlencoded when body_urlencoded is used; Accept-Encoding when decompress is active; Range when response_save_resume is set and the target file already exists. Only emitted when there are implicit headers to report.