Skip to main content

trillium client

A curl-like HTTP client that pretty-prints JSON, streams bodies, follows redirects by default, and negotiates HTTP/2 over TLS via ALPN.

trillium client <METHOD> <URL> [OPTIONS]
trillium client get https://example.com
trillium client get https://httpbin.org/json

The method is case-insensitive (get, GET, Get all work). A URL with no scheme is assumed to be http://.

Output

When stdout is a terminal, client formats for humans:

  • JSON responses (any application/json or +json content type) are pretty-printed with syntax-colored output.
  • Other bodies stream straight to stdout.
  • A colored Status: line is printed for non-200 responses.

When stdout is not a terminal (piped or redirected), the colorized status line and request log are suppressed and JSON is emitted as plain pretty-printed text, so it composes cleanly with jq and friends:

trillium client get https://httpbin.org/json | jq .slideshow.title

Raise the verbosity with -v to also print the request line, request and response headers, peer address, negotiated version, and any response trailers.

A one-line-per-request log is shown automatically when stdout is a terminal, and suppressed when output is piped or redirected so it can't corrupt the response body. Pass --always-log to force it on regardless — it then writes to stderr, keeping stdout clean. This is handy for watching --retry attempts in a script.

Request bodies

Provide a body inline, from a file, or from stdin — the three forms are equivalent:

trillium client post https://httpbin.org/anything -b '{"hello": "world"}'
trillium client post https://httpbin.org/anything -f ./body.json
cat ./body.json | trillium client post https://httpbin.org/anything
FlagNotes
-b, --bodyinline request body string
-f, --fileread the body from a file (streamed)
(stdin)a piped/redirected stdin is streamed as the body

File and stdin bodies are streamed, not buffered, so they're fine for large uploads.

Headers

Repeat -H KEY=VALUE for each header:

trillium client post https://httpbin.org/anything \
-H Authorization="Bearer $TOKEN" -H Content-Type=application/json \
-b '{"hello": "world"}'

Compression

-c / --compression compresses the request body with the given encoding before sending it:

trillium client post https://api.example.com -f ./big.json -c zstd
ValueEncoding
zstdZstandard
brBrotli (alias brotli)
gzipgzip

Responses are always decoded transparently regardless of this flag — it only controls the outbound body. There is no content negotiation for request bodies, so only use it against an origin you know accepts the encoding.

Saving the body

-o / --output-file writes the response body to a file instead of stdout. With no argument it derives the filename from the URL's last path segment:

trillium client get https://example.com/report.pdf -o # → ./report.pdf
trillium client get https://example.com/report.pdf -o out.pdf # → ./out.pdf

Server-Sent Events

--sse reads the response as a Server-Sent Events stream. It sends Accept: text/event-stream, then prints each event as it arrives instead of buffering a single body, staying open until the server closes the connection:

trillium client get https://example.com/events --sse

The output adapts to where it's going, the same way the rest of client does:

  • At a terminal, each event is rendered with colored field labels — its event type, and the id and retry decorators when present — and a JSON data payload is pretty-printed with syntax coloring.
  • Piped or redirected, events are emitted as newline-delimited JSON, one object per line ({event?, id?, retry_ms?, data}, with data kept verbatim), so the stream composes with jq and friends:
trillium client get https://example.com/events --sse | jq .data

The per-request --timeout only bounds opening the stream (receiving the response head), not the events that follow, so a long-lived feed isn't cut off at the default 10s. The stream does not auto-reconnect; pair it with --retry to retry a stream that never opens.

Redirects

Redirects are followed automatically (up to 10 hops). HTTPS→HTTP downgrades are refused unless you opt in.

FlagDefaultNotes
--no-follow-redirectsprint the 3xx response as-is instead of following
--max-redirects10maximum hops before erroring
--allow-downgradepermit following an https://http:// redirect

TLS and HTTP version

trillium client get https://example.com --http-version 2
trillium client get https://localhost:8443 -k # self-signed dev cert
FlagDefaultNotes
-t, --tlsrustlsclient TLS backend: rustls/native/openssl/none
--http-version1.10.9, 1.0, 1.1, 2, or 3 (h3 requires the h3 feature)
-k, --insecureskip certificate verification (rustls only) — dangerous

Over TLS, HTTP/2 is negotiated via ALPN when the server supports it; pass --http-version to force a specific protocol. Requests to https:// URLs with --tls none will fail.

Timeouts and dry runs

trillium client get https://slow.example.com --timeout 30s
trillium client post https://api.example.com -b '{}' --dry-run
FlagDefaultNotes
--timeout10sper-request timeout (e.g. 30s, 1m, 500ms)
--no-timeoutdisable the per-request timeout entirely
--dry-runprint the request that would be sent, then exit

--dry-run is handy for inspecting exactly what would go over the wire — method, URL, headers, and body — without making a request.

Retries

--retry N retries a failed request up to N times (the default 0 disables retries entirely):

trillium client get https://flaky.example.com --retry 3

Retries cover transport errors (connection refused, reset, timeout) and the retryable statuses 429 Too Many Requests and 503 Service Unavailable, using exponential backoff and honoring a server-advertised Retry-After.

Only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE) are retried unless you pass --retry-all-methods. A body streamed from stdin or --file can't be replayed, so such a request is never retried — use --body for a retryable body.

FlagDefaultNotes
--retry0maximum retry attempts (0 disables)
--retry-delayfixed delay between retries instead of exponential backoff (e.g. 500ms)
--retry-max-time30stotal wall-clock budget across all attempts
--retry-all-methodsalso retry non-idempotent methods (POST, PATCH)

Keep --retry-max-time at least as large as --timeout: the first attempt uses the per-request timeout, and the budget caps every attempt after it. --retry-all-methods is only safe when the endpoint is idempotent in practice (or guarded by an idempotency key), since replaying it may duplicate a side effect.

To watch retry attempts in a script, pass --always-log (see Output).

Full flag reference

trillium client [OPTIONS] <METHOD> <URL>

Arguments:
<METHOD> HTTP method (case-insensitive)
<URL> Request URL (http:// assumed if no scheme)

Options:
-b, --body <BODY>
-f, --file <FILE>
-o, --output-file [<OUTPUT_FILE>]
--sse stream the response as Server-Sent Events
-H, --headers <HEADERS> KEY=VALUE, repeatable
-c, --compression <COMPRESSION> compress the request body: zstd, br, gzip
-t, --tls <TLS> [default: rustls]
--http-version <HTTP_VERSION> [default: 1.1]
-k, --insecure
--dry-run
--always-log
-v, --verbose...
-q, --quiet...
-h, --help

Timeout:
--timeout <TIMEOUT> [default: 10s]
--no-timeout

Redirects:
--no-follow-redirects
--max-redirects <MAX_REDIRECTS> [default: 10]
--allow-downgrade

Retries:
--retry <N> [default: 0]
--retry-delay <RETRY_DELAY>
--retry-max-time <RETRY_MAX_TIME>
--retry-all-methods