Hurl
Hurl runs HTTP requests defined in a plain-text format and asserts on the responses. One binary (hurl, built on libcurl), one readable file format: a .hurl file is a sequence of entries, each a request plus an optional response spec with asserts and captures. The same files work as smoke tests, integration tests, CI gates, and living API documentation — they read like the API contract they verify.
Current version: 8.0.1. Reference material in this skill is verified against hurl.dev for that release.
A minimal entry:
GET https://api.example.org/health HTTP 200 [Asserts] jsonpath "$.status" == "RUNNING"
Entries in one file run sequentially and share cookies and captured variables (session behavior). Files are independent and run in parallel under --test. That asymmetry drives all suite design: one user flow per file.
- Header vs section placement changes meaning.
key: valuelines directly after the URL are request headers — no section name. The same line inside[Query]is a query parameter. Body always comes last;[Form]/[Multipart]are the body and exclude a literal one. - Use current section names
[Query],[Form],[Multipart]. The legacy[QueryStringParams]/[FormParams]/[MultipartFormData]still parse — recognize them, don't write them. - Predicates are typed.
== trueis a boolean check,== "true"a string check;== 458number,== "458"string. Same rule for templates:== {{count}}compares typed,== "{{count}}"as string.matcheson a non-string is a runtime error — convert withtoInt/toStringfirst. HTTP 200is itself an assert (any version, status 200).HTTP/2 200also asserts the protocol. UseHTTP *plus explicitstatus >= 200asserts for ranges. Asserts run with or without--test— test mode only changes output and adds the recap.- Redirects are not followed by default. Either assert each hop as its own entry, or set
[Options] location: true(or--location) and assert on the final response — thenredirects count/redirects nth 0 locationaudit the chain andurlgives the final URL. retryturns asserts into wait conditions.[Options] retry: 10+retry-interval: 500msre-runs the entry until its asserts pass — the polling primitive for async jobs and server-readiness gates. Retries trigger on assert, capture, and runtime errors.- Option precedence is env < CLI flag <
[Options]section.[Options]applies to its entry only, exceptvariable:which persists to later entries.skip: trueexists only in[Options];--test,--report-*,--variables-file,--secretare CLI-only. - Secrets, not variables, for credentials.
--secret name=value,HURL_SECRET_name, or a trailingredacton a capture redacts the value from logs and reports. Redaction is exact-match (register transformed variants separately) and does NOT apply to stdout or JSON-report response dumps. - Reports accumulate. Every
--report-html/json/junit/tapappends to an existing report. Clean the report path at the start of each CI run. - Regex literals beat quoted patterns.
/^\d{4}$/needs no double escaping;"^\\d{4}$"does. Theregexquery and filter assert/extract the first capture group — a pattern without a group fails. - Templates resolve variables only —
{{host}},{{newUuid}},{{newDate}}; no arithmetic or expressions. Compute in shell and inject with--variableorHURL_VARIABLE_name. Raw XML bodies are not template-aware; fence them as```xmlmultiline strings to template them. - 8.0 changes to honor: JSONPath engine is now RFC 9535 (edge cases differ from pre-8.0),
--interactivewas removed (use--from-entry/--to-entry, per-entry[Options] verbose: true,--curlreplay), env vars areHURL_VARIABLE_name(old bareHURL_nameform is gone),decode/formatfilters are deprecated forcharsetDecode/dateFormat.
Build files incrementally — each step is runnable, so verify as you go.
- Smoke first. One entry, status only:
GET {{host}}/health+HTTP 200. Run it:hurl --variable host=http://localhost:3000 health.hurl. - Add asserts. Tighten the contract with
[Asserts]:jsonpath/xpath/headerqueries, typed predicates. Prefer explicit asserts over an exact body literal unless you want golden-file equality. - Chain the flow. Capture what later entries need (
[Captures]token: jsonpath "$.access_token" redact), use it via{{token}}. Cookies flow automatically. - Handle async. Entries that poll get
[Options] retry: N+retry-interval, with the completion condition as an assert. - Parameterize. Replace hosts and credentials with
{{variables}}; create per-environment variables files (vars/local.env,vars/staging.env); pass tokens as secrets. - Run as a suite.
hurl --test tests/(parallel files, recap, exit code 4 on assert failure). Add--error-format longso failures log the actual response. - Wire CI.
--report-junitfor ingestion,--report-htmlfor humans; clean report paths first; gate readiness with a stdin one-liner:printf 'GET %s\nHTTP 200' "$URL" | hurl --retry 60 --retry-interval 2s.
When a file fails: re-run with --very-verbose --to-entry N to isolate the entry with full bodies on stderr, or hurl --curl repro.txt file.hurl to export the exact requests as curl commands.
POST {{host}}/api/login {"username": "{{user}}", "password": "{{password}}"} HTTP 200 [Captures] token: jsonpath "$.access_token" redact GET {{host}}/api/me Authorization: Bearer {{token}} HTTP 200 [Asserts] jsonpath "$.username" == "{{user}}"
Run: hurl --variable host=http://localhost:3000 --variable user=bob --secret password=$PASS login.hurl
POST {{host}}/jobs HTTP 201 [Captures] job_id: jsonpath "$.id" GET {{host}}/jobs/{{job_id}} [Options] retry: 10 retry-interval: 500ms HTTP 200 [Asserts] jsonpath "$.state" == "COMPLETED"
rm -rf build/hurl-report build/hurl-junit.xml hurl --test \ --variables-file vars/staging.env \ --error-format long \ --report-junit build/hurl-junit.xml \ --report-html build/hurl-report \ tests/
echo "curl -X POST https://api.example.org/users -H 'Content-Type: application/json' -d '{\"name\":\"bob\"}'" | hurlfmt --in curl
References
- File Format -- Entries, request/response anatomy, all sections and body types, templating, grammar gotchas
- Asserting -- Every query type and predicate, implicit vs explicit asserts, typing rules
- Captures and Filters -- Capture syntax and scope, the full filter table, chaining, secrets and redaction
- CLI -- hurl and hurlfmt: invocation, test mode, all option groups, reports, exit codes, [Options] precedence
- Testing Workflows -- Suite layout, retry-until polling, environments, CI integration, debugging, executable-docs export
- Recipes -- 30 complete runnable examples: auth flows, chained requests, uploads, GraphQL, CI snippets

