Hurl HTTP tests: catch a false green
Use Hurl to send an HTTP request and assert the response code, headers and JSON values your caller needs. Start with a local fixture, confirm a healthy response passes, then deliberately break it. A green status-only check can hide a wrong payload; this walkthrough makes that failure visible before you adopt the check.
By Built with Rust editorial ยท Published
Hurl is a Rust command-line tool for HTTP requests and response assertions. This exercise is for developers deciding whether plain-text API checks fit their workflow. You do not need a Rust compiler or a public API account. The original contribution is a small, reproducible response fixture with deliberately broken modes, not another list of HTTP clients.
- Download the loopback fixture and check the prerequisites
- Run a status-only check against a healthy response
- Add explicit header and JSON assertions
- Deliberately break the response and inspect the failure
- Move one verified contract into your own workflow
- What was checked, and what was not
- Common questions
Scroll the diagram sideways, or focus it and use the arrow keys.
1. Prepare an isolated local fixture
You need Hurl and Python 3, plus two terminal windows. Follow the official installation instructions for your operating system. The walkthrough was checked with Hurl 8.0.1 on Linux and Python 3.11.2 on October 25, 2026. That is a tested combination, not a claim that every operating system or later release behaves identically. On Windows, use your Python launcher's equivalent if python3 is not available.
Download these three files into a new empty folder. Read the short server before running it. It serves a synthetic JSON response, binds only to 127.0.0.1, and does not require credentials or your project's files. Nothing is uploaded to Built with Rust. The server exists only while its process is running.
- server.py: local fixture
- status-only.hurl: response-code check
- contract.hurl: header and payload checks
Run the server from that folder in terminal A. Leave it running while using terminal B for Hurl. If port 8765 is already occupied, stop here and resolve the conflict rather than assuming the process answering there is this fixture. These commands are for this page's versioned fixture; its Python HTTP-server API is documented in the Python 3.11 reference.
python3 server.py --mode healthyThe process should report that it is listening on 127.0.0.1:8765. Stop it with Ctrl+C before changing modes. Do not expose this demonstration server through a public interface or treat it as a production application.
2. Get a green status-only check
The first Hurl file asks for the fixture's health response and requires HTTP 200. It deliberately says nothing about the payload. With the healthy server running, execute the following in terminal B. Hurl's test-running reference and manual describe these options; the commands and downloaded files were exercised with version 8.0.1.
hurl --test --jobs 1 --connect-timeout 2 --max-time 3 status-only.hurlExpect a successful file result and process exit code 0. The timeouts bound waiting for this tiny local example; they are not a latency target for a real service. The single-job option runs at most one input file at a time; it does not establish dependencies between files or add checks to the response.
Read the downloaded file before adding assertions. Its HTTP 200 line is already an assertion. Hurl executes assertions without --test too; test mode supplies a summary and changes normal response output. A file that succeeds has established only the properties it actually asserted.
3. Check the response contract
A response contract is the small set of properties the caller relies on. Here the caller requires JSON, a status field with the string value ok, and an items field with the number 2. The second file adds those requirements to the same request. The syntax comes from Hurl's response-assertion reference; this is the exact downloadable file tested with Hurl 8.0.1.
# Original Built with Rust fixture, verified with Hurl 8.0.1.
# https://hurl.dev/docs/running-tests.html
# https://hurl.dev/docs/asserting-response.html
GET http://127.0.0.1:8765/health
HTTP 200
[Asserts]
header "Content-Type" contains "application/json"
jsonpath "$.status" == "ok"
jsonpath "$.items" == 2
Run it while the server is still healthy. The same version-8.0.1 manual and test-running documentation apply to this invocation.
hurl --test --jobs 1 --connect-timeout 2 --max-time 3 contract.hurlExpect success again. The JSONPath queries select individual response fields; equality checks their values. Keep the number unquoted when the contract requires a number. The content-type check accepts a JSON header with an additional charset parameter, but it is only a contains check, not a complete media-type parser. These assertions are intentionally narrow: they do not describe every valid API response or replace a schema designed for your application.
4. Prove the test catches a bad response
Stop terminal A with Ctrl+C. Restart the same fixture in wrong-json mode, then run both Hurl commands again without editing either test file. This mode still answers HTTP 200 with JSON, but changes the status field. The command below is part of this page's versioned Python fixture.
python3 server.py --mode wrong-jsonThe status-only file still succeeds. The contract file fails its JSON assertion. That difference is the false green: reachability looked healthy while a caller's expected value was wrong. Inspect the reported assertion and the process exit status rather than treating any printed response as success. Repeat with the other modes below, stopping the old process before each restart. Pass the mode name after --mode when starting server.py.
Scroll the table sideways, or focus it and use the arrow keys.
| Fixture mode | Status-only file | Contract file | What it isolates |
|---|---|---|---|
| healthy | Pass | Pass | Expected synthetic response |
| wrong-json | Pass | Fail | Wrong value despite HTTP 200 |
| wrong-header | Pass | Fail | Wrong content type despite parseable JSON |
| wrong-status | Fail | Fail | HTTP 503 instead of 200 |
| malformed-json | Pass | Fail | HTTP 200 with an invalid JSON body |
| Server stopped | Fail | Fail | No connection; no assertion result |
A failed connection is not proof that the server returned a bad JSON value: no response was available to check. Likewise, a failed header assertion identifies a different requirement from a failed status assertion. Keep these failure classes distinct when deciding what to investigate. This table reports this fixture and these files, not an estimate of reliability across websites.
5. Move one useful check into your workflow
Choose one endpoint you control and one caller expectation worth protecting. Work on a disposable branch or staging setup. Change the URL and assertions to match that contract, including its expected status, rather than copying this health response into an unrelated API. First verify a known-good response, then make a reversible change that should violate the contract. If the check stays green, improve the assertion before relying on it in CI.
Keep the first trial independent of unrelated changes. The terminal-workflow guide separates terminal, shell and history choices; none is required to adopt Hurl. Browse other Rust developer tools if the problem is linting, bundling or editing instead of checking HTTP responses. Hurl is an HTTP client here, not your application's implementation language.
Preserve TLS certificate verification when moving beyond loopback; this plain-HTTP fixture does not exercise TLS or authentication. Avoid putting live tokens or private response bodies into committed example files, logs or shared reports. Test mode is not a privacy boundary: debug output, reporting options and other output settings can still disclose response content. Review what your chosen CI command stores, and keep credentials in the environment or your established secret mechanism.
Once the good response passes and a deliberate defect fails, keep the check small enough that its purpose is obvious in review. A successful smoke check does not establish browser behavior, load capacity or security. Those need their own tests. Stop the local server with Ctrl+C when finished; the downloaded folder can be removed when you no longer need the exercise. Visit the Hurl project listing for its website and source links, or submit a Rust-powered tool that solves a different workflow.
What was checked, and what was not
Built with Rust ran the two downloadable files against each named mode of its own Python loopback fixture on Linux, using Hurl 8.0.1 and Python 3.11.2. The server was restarted between modes, and connection refusal was checked after stopping it. The files shown above are the same embedded assets available for download. No private account data, production API or remote credentials were used. These are reproducible functional examples, not a performance benchmark, cross-platform certification or security audit.
If your local result differs, check the installed Hurl version, which process owns the port, the selected mode and the exact downloaded file before changing your application. Use the Hurl 8.0.1 release for the tested release and the current official documentation for later syntax. A future version or a different network environment deserves a fresh check.
Common questions
Do Hurl assertions need --test?
No. Hurl executes assertions with or without --test. Test mode changes output and provides a run summary; it also runs multiple input files in parallel unless you select --jobs 1.
Why can an HTTP 200 test pass the wrong JSON?
A status assertion checks the response code, not the meaning of the body. Add assertions for the response fields and headers your caller actually requires, then deliberately break those values to verify the check fails.
Does this exercise prove my API is production-ready?
No. It tests one synthetic response contract on loopback. It does not establish authentication, TLS, browser rendering, concurrency, resilience or security for a real service.