yurl
yurl is an HTTP client built for clarity, supporting one-off and batch requests with concurrency and streaming.
Built on yttp, the "Better HTTP" JSON/YAML façade. Adds flexible output routing and rule-based middleware.
Want JSON output? Use jurl - same binary.
Shortcuts · Auth · Output · Concurrency · Progress · Batch config · Cookbook · Reference
Install with: cargo install yurl
If the first examples make sense, jump to the Reference.

|
Output, edited for brevity:
s: # status (inline by default)
h: # response (output) headers
content-type: application/json
server: cloudflare
b: # response (output) body — JSON preserved as structure
id: 1 # ← API response content, not jurl
title: sunt aut facere...
userId: 1
Batch mode:
|
Multiline YAML helps with readability, but single-line JSON is also fine.
Shared settings for batched requests can be passed as a CLI argument:
# save config to file
# send requests — each YAML document (---) becomes one JSONL line
Request
Reads requests from stdin as JSON (one per line) or YAML (documents separated by ---).
The HTTP method key holds the URL. Any capitalization is accepted; g, p, d are shortcuts for get, post, delete.
echo '{p: https://httpbin.org/post, b: {key: val}}' | jurl
Request keys
- HTTP method (
get,post,put,delete,patch,head,options,trace) — URL h/headers— request (input) headers (keys and values support shortcuts, see below)b/body— request (input) body (encoding determined by Content-Type)md— arbitrary metadata (any JSON value), echoed into output
Body encoding
The Content-Type header determines how b is encoded:
application/json(default,c!: j!is implied) — JSON bodyapplication/x-www-form-urlencoded— form encoding (bobject becomeskey=value&...)multipart/form-data— multipart encoding; values starting withfile://are read from disk
Authorization
The a! (or auth!) key inside h sets the Authorization header. The auth scheme is inferred from the value type:
Bearer — pass a string token:
echo '{g: https://httpbin.org/get, h: {a!: my-token}}' | jurl
# → Authorization: Bearer my-token
Basic — pass credentials as an array:
echo '{g: https://httpbin.org/get, h: {a!: [user, pass]}}' | jurl
# → Authorization: Basic dXNlcjpwYXNz
Other schemes — if the string already contains a scheme prefix (has a space), it's passed through as-is:
echo '{g: https://httpbin.org/get, h: {a!: Digest abc123}}' | jurl
# → Authorization: Digest abc123
The explicit basic! and bearer! value prefixes also still work:
echo '{g: https://httpbin.org/get, h: {a!: basic!user:pass}}' | jurl
echo '{g: https://httpbin.org/get, h: {a!: [user, pass]}}' | jurl
echo '{g: https://httpbin.org/get, h: {a!: bearer!my-token}}' | jurl
echo '{g: https://httpbin.org/get, h: {a!: my-token}}' | jurl
In multi-line YAML:
h:
a!: my-token # bearer (most common)
# or
a!: # basic
# or
a!: Digest abc123 # explicit scheme
a! works inside h: in requests, config defaults, and rules.
Header shortcuts
Shortcuts expand in header keys and values:
| Shortcut | Expands to |
|---|---|
json! / j! |
application/json |
form! / f! |
application/x-www-form-urlencoded |
multi! / m! |
multipart/form-data |
html! / h! |
text/html |
text! / t! |
text/plain |
xml! / x! |
application/xml |
a!/suffix |
application/suffix |
t!/suffix |
text/suffix |
i!/suffix |
image/suffix |
basic!user:pass |
Basic base64(user:pass) |
bearer!token |
Bearer token |
| Key shortcuts | |
a! / auth! |
Authorization (header key) |
c! / ct! |
Content-Type (header key) |
echo '{g: https://httpbin.org/get, h: {a!: basic!user:pass}}' | jurl
echo '{g: https://httpbin.org/get, h: {Accept: a!/xml}}' | jurl
Output
By default, jurl writes j(s!,h,b) (JSON with body, headers, and status) to stdout.
To customize output, add to the request JSON key-value pairs like so:
- key: the destination —
"1"(stdout),"2"(stderr), or"file://path"(supports{{atom}}templates) - value: what to write — a raw atom like
bors, orj(...)to output them as JSON
Multiple destinations can be used in a single request. In the unlikely case that the files resolve to the same name, the last value wins.
Format
Atoms reference parts of the response (output) or request (input):
Response (output) — the default, most common:
b/o.b— response (output) body:- outside
j()/y(): raw bytes - inside
j()/y(): smart encoding — JSON body → embedded as structured value, UTF-8 text → string, binary → base64 string
- outside
h/o.h— response (output) headers (raw HTTP format outsidej(), JSON object insidej())s/o.s— response status line;s.code,s.text,s.versionfor parts
Request (input) — echo what was sent:
i.b— request (input) bodyi.h— request (input) headersi.s— request status line
Other:
m— request methodu— full request URLidx— auto-incrementing request index (0-based)md— metadata (entire value);md.x,md.y→ grouped as"md": {"x": ..., "y": ...}
URL parts and metadata are available for file path templates: u.scheme, u.host, u.port, u.path, u.query, u.fragment, idx, md, md.*.
j(...) wraps atoms into a JSON object.
Default output (when no destination key is present): {"1": "j(s!,h,b)"}
Examples
echo '{g: https://httpbin.org/get}' | jurl | jq .b # body is structured JSON, not base64
echo '{g: https://httpbin.org/get, 1: b}' | jurl
echo '{g: https://httpbin.org/get, 1: j(s.code,s.text)}' | jurl
echo '{g: https://httpbin.org/get, file://./out/{{u.host}}/{{m}}.txt: b}' | jurl
Batch config
An optional CLI argument provides shared configuration for all requests: default headers, output format, and conditional rules.
jurl '{h: {a!: bearer!tok}}'
All stdin requests inherit these headers. Per-request (input) headers override config headers.
Shortcuts (c!/ct!, a!/auth!, value shortcuts) work in config and rules too.
Rules
Rules conditionally add headers based on URL, method, or metadata matching.
# save config to file
# use config
|
u— URL glob (*matches non-/,**matches anything)m— HTTP method (exact, case-insensitive)md.<field>— exact metadata field match
Merge order: config defaults → matching rules (in order) → per-request.
Concurrency and streaming
By default, requests run sequentially (concurrency: 1). Set concurrency in batch config to run requests in parallel:
jurl '{concurrency: 10}'
Per-endpoint limits can be set via rules — the request must satisfy both the global and per-endpoint limit:
When running requests concurrently, outputs could interleave. jurl handles this automatically:
- File outputs with
{{idx}}in the path are guaranteed unique per request. These are streamed directly to disk — no buffering, constant memory regardless of response size. - File outputs without
{{idx}}could collide across requests, so they are buffered and written atomically, unless otherwise stated with?stream - stdout/stderr with
concurrency: 1— streamed directly (no interleaving risk with a single request in flight). - stdout/stderr with
concurrency > 1— buffered and flushed atomically to prevent interleaving.
Override with ?stream. to force streaming on a file destination that jurl would otherwise buffer:
- a static path you know is only used by one request
- a dynamic path that acquires its uniqueness from other components apart from
{{idx}}, e.g. some{{md.*}}
Note that when concurrency is 1, as is the case for one-off requests, streaming is automatically enabled, so you don't need to worry about
large payloads causing OOM errors.
When streaming, the body is written chunk-by-chunk as it arrives. If another (non-streaming) destination also needs the body, it is still buffered for that destination — but the streaming file never accumulates the full response in memory.
Progress
Set progress in batch config to show a progress bar on stderr:
jurl '{progress: true}'
If the number of requests is known, pass it as a number for a proper progress bar instead of a spinner:
jurl '{progress: 100, concurrency: 10}'
When progress is active, any request output directed to stderr ("2") is silently suppressed. A warning line appears below the progress bar showing how many requests had their stderr output suppressed.
Cookbook
Simple GET — default output is JSON with body, headers, status:
$ echo '{g: https://httpbin.org/get}' | jurl
{"s": {"v": "HTTP/1.1", "c": 200, "t": "OK"}, "h": {"content-type": "application/json", ...}, "b": {"url": "https://httpbin.org/get", ...}}
Raw body to stdout:
$ echo '{g: https://httpbin.org/get, 1: b}' | jurl
{"args": {}, "headers": {"Host": "httpbin.org", ...}, "url": "https://httpbin.org/get"}
Just the status line:
$ echo '{g: https://httpbin.org/get, 1: s}' | jurl
HTTP/1.1 200 OK
Status code and text as JSON:
$ echo '{g: https://httpbin.org/get, 1: j(s.code,s.text)}' | jurl
{"s": {"c": 200, "t": "OK"}}
POST with JSON body (default encoding). c! is optional since JSON is the default, but json! / j! work:
$ echo '{p: https://httpbin.org/post, b: {key: val}, 1: b}' | jurl
$ echo '{p: https://httpbin.org/post, h: {c!: j!}, b: {key: val}, 1: b}' | jurl
{..."json": {"key": "val"}...}
Form POST — full header, then with form! / f!:
# full Content-Type header
|
# shortcut
|
# output (all three)
}
Multipart upload — full header, then with multi! / m!:
# full Content-Type header
|
# shortcut
|
# output (all three)
}
Basic auth — full header, then basic! shortcut:
$ echo '{g: https://httpbin.org/get, h: {Authorization: Basic dXNlcjpwYXNz}, 1: b}' | jurl
$ echo '{g: https://httpbin.org/get, h: {a!: basic!user:pass}, 1: b}' | jurl
$ echo '{g: https://httpbin.org/get, h: {a!: [user, pass]}, 1: b}' | jurl
{..."headers": {..."Authorization": "Basic dXNlcjpwYXNz"...}...}
Bearer auth — full header, then bearer! shortcut:
$ echo '{g: https://httpbin.org/get, h: {Authorization: Bearer tok123}, 1: b}' | jurl
$ echo '{g: https://httpbin.org/get, h: {a!: bearer!tok123}, 1: b}' | jurl
{..."headers": {..."Authorization": "Bearer tok123"...}...}
MIME prefix shortcuts — a!/, t!/, i!/:
$ echo '{g: https://httpbin.org/get, h: {Accept: a!/xml}, 1: b}' | jurl
{..."headers": {..."Accept": "application/xml"...}...}
$ echo '{g: https://httpbin.org/get, h: {Accept: t!/csv}, 1: b}' | jurl
{..."headers": {..."Accept": "text/csv"...}...}
$ echo '{g: https://httpbin.org/get, h: {Accept: i!/png}, 1: b}' | jurl
{..."headers": {..."Accept": "image/png"...}...}
Named shortcuts — long and short forms:
$ echo '{g: https://httpbin.org/get, h: {Accept: x!}, 1: b}' | jurl
{..."headers": {..."Accept": "application/xml"...}...}
$ echo '{g: https://httpbin.org/get, h: {Accept: h!}, 1: b}' | jurl
{..."headers": {..."Accept": "text/html"...}...}
$ echo '{g: https://httpbin.org/get, h: {Accept: t!}, 1: b}' | jurl
{..."headers": {..."Accept": "text/plain"...}...}
Metadata — scalar, object, and field selection:
|
}
# YAML with metadata object
}
# selecting specific metadata fields
|
}
JSONL — multiple requests, idx auto-increments:
$ printf '{"g":"https://httpbin.org/get","1":"j(idx,s.code)"}\n{"g":"https://httpbin.org/get","1":"j(idx,s.code)"}\n' | jurl
{"idx": 0, "s": {"code": 200}}
{"idx": 1, "s": {"code": 200}}
Default output format in config — requests don't need to repeat it:
$ printf '{g: https://httpbin.org/get}\n{p: https://httpbin.org/post, b: {x: "1"}}\n' | jurl '{1: j(idx,m,s.code)}'
{"idx": 0, "m": "GET", "s": {"code": 200}}
{"idx": 1, "m": "POST", "s": {"code": 200}}
Per-request output overrides the config default:
$ printf '{g: https://httpbin.org/get}\n{g: https://httpbin.org/get, 1: s}\n' | jurl '{1: j(idx,s.code)}'
{"idx": 0, "s": {"code": 200}}
HTTP/1.1 200 OK
Multiple destinations — body to file, headers to stdout, status to stderr:
# stdout
}
# stderr
# body.out contains the raw response (output) body
Templated file output:
# writes response (output) body to ./out/httpbin.org/GET.txt
Config — default auth for all requests:
$ echo '{g: https://httpbin.org/get, 1: b}' | jurl '{h: {a!: bearer!session-tok}}'
{..."headers": {..."Authorization": "Bearer session-tok"...}...}
Config — rule adds form encoding to all POSTs:
$ echo '{p: https://httpbin.org/post, b: {x: "1"}, 1: b}' | jurl '{rules: [{match: {m: POST}, h: {c!: f!}}]}'
{..."form": {"x": "1"}...}
Config — rule matches metadata:
}
Per-request headers override config:
$ echo '{g: https://httpbin.org/get, h: {X-Val: custom}, 1: b}' | jurl '{h: {X-Val: default}}'
{..."headers": {..."X-Val": "custom"...}...}
Config — two APIs with different tokens, matched by URL:
# output
}
}
Reference
Commented YAML schema by example, not a valid request.
"Better HTTP" - request and response (yttp)
Full specification: yttp reference — method shortcuts, header shortcuts (a!, c!, value shortcuts), auth, body encoding, and response formatting.
# request
g: https://example.com # method shortcuts: g p d, or full names
h: # header key/value shortcuts expand in place
b: # body encoding follows Content-Type
# response — default output: y(s!,h,b)
s: # s! → status inline object
h: # response (output) headers
b: # JSON → structured, UTF-8 → string, binary → base64
jurl extensions
# ============================
# METADATA
# ============================
md: # arbitrary value, available in output and file path templates
env: prod # {{md.env}}, md.env in j()/y()
batch: 7 # {{md.batch}}, md.batch in j()/y()
# ============================
# OUTPUT DESTINATIONS
# ============================
1: j(s!,h,b) # fd 1 (stdout) ← default for jurl
1: y(s!,h,b) # fd 1 (stdout) ← default for yurl
2: s # fd 2 (stderr) ← raw status line
file://response.raw: b # file ← raw body
file://{{md.env}}/{{idx}}.json: j(s!,h,b) # file ← templated path, auto-streamed (has {{idx}})
file://large.bin?stream: b # file ← explicit streaming (no buffering)
# ============================
# OUTPUT ATOMS
# ============================
# response (output): b h s! s s.c s.t s.v (or s.code s.text s.version)
# or: o.b o.h ...
# request (input): i.b i.h i.s
# URL parts: u.scheme u.host u.port u.path u.query u.fragment
# other: m u idx md md.*
# body encoding in j()/y():
# JSON body → embedded as structured value (object/array)
# UTF-8 text → embedded as string
# binary → base64-encoded string
# (detect by type: object/array = JSON, string = text or base64, check h for Content-Type)
# output formats:
# j(s!,h,b) → JSON object with selected atoms
# y(s!,h,b) → YAML object with selected atoms
# b → raw (no wrapping)
Batch config (middleware)
Passed as a CLI argument. Acts as middleware — applied to every request before it's sent.
# --- Default headers ---
h:
a!: bearer!my-token # applied to all requests
User-Agent: jurl/0.1
# --- Default output ---
1: j(idx, s.code) # applied when request has no output keys
# --- Concurrency ---
concurrency: 10 # global max in-flight requests (default: 1)
# --- Progress ---
progress: true # spinner (unknown count)
progress: 100 # progress bar (known count)
# suppresses stderr output, shows warning count
# --- Rules ---
rules:
- match: # URL glob (* = segment, ** = any)
concurrency: 2 # per-endpoint concurrency limit
- match: # method match (case-insensitive)
h: # add/override headers
- match: # metadata field match (exact)
h:
- match: # multiple criteria (AND)
h:
# merge order: config defaults → matching rules (in order) → per-request