Hurl is a command line tool that runs HTTP requests defined in a simple plain text format.
It can chain requests, capture values and evaluate queries on headers and body response. Hurl is very versatile: it can be used for both fetching data and testing HTTP sessions.
Hurl makes it easy to work with HTML content, REST / SOAP / GraphQL APIs, or any other XML / JSON based APIs.
# Get home: GET https://example.org HTTP 200 [Captures] csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)" # Do login! POST https://example.org/login?user=toto&password=1234 X-CSRF-TOKEN: {{csrf_token}} HTTP 302
Chaining multiple requests is easy:
GET https://example.org/api/health GET https://example.org/api/step1 GET https://example.org/api/step2 GET https://example.org/api/step3
Also an HTTP Test Tool
Hurl can run HTTP requests but can also be used to test HTTP responses. Different types of queries and predicates are supported, from XPath and JSONPath on body response, to assert on status code and response headers.
It is well adapted for REST / JSON APIs
POST https://example.org/api/tests { "id": "4568", "evaluate": true } HTTP 200 [Asserts] header "X-Frame-Options" == "SAMEORIGIN" jsonpath "$.status" == "RUNNING" # Check the status code jsonpath "$.tests" count == 25 # Check the number of items jsonpath "$.id" matches /\d{4}/ # Check the format of the id
HTML content
GET https://example.org HTTP 200 [Asserts] xpath "normalize-space(//head/title)" == "Hello world!"
GraphQL
POST https://example.org/graphql ```graphql { human(id: "1000") { name height(unit: FOOT) } } ``` HTTP 200
and even SOAP APIs
POST https://example.org/InStock Content-Type: application/soap+xml; charset=utf-8 SOAPAction: "http://www.w3.org/2003/05/soap-envelope" GOOG HTTP 200
Hurl can also be used to test the performance of HTTP endpoints
GET https://example.org/api/v1/pets HTTP 200 [Asserts] duration < 1000 # Duration in ms
And check response bytes
GET https://example.org/data.tar.gz HTTP 200 [Asserts] sha256 == hex,039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81;
Finally, Hurl is easy to integrate in CI/CD, with text, JUnit, TAP and HTML reports
Why Hurl?
Text Format: for both devops and developers
for both devops and developers Fast CLI: a command line for local dev and continuous integration
a command line for local dev and continuous integration Single Binary: easy to install, with no runtime required
Powered by curl
Hurl is a lightweight binary written in Rust. Under the hood, Hurl HTTP engine is powered by libcurl, one of the most powerful and reliable file transfer libraries. With its text file format, Hurl adds syntactic sugar to run and test HTTP requests, but it's still the curl that we love: fast, efficient and IPv6 / HTTP/3 ready.
Feedbacks
To support its development, star Hurl on GitHub!
Feedback, suggestion, bugs or improvements are welcome.
POST https://hurl.dev/api/feedback { "name": "John Doe", "feedback": "Hurl is awesome!" } HTTP 200
Resources
License
Blog
Tutorial
Documentation (download HTML, PDF, Markdown)
GitHub
Table of Contents
Samples
To run a sample, edit a file with the sample content, and run Hurl:
$ vi sample.hurl GET https://example.org $ hurl sample.hurl
By default, Hurl behaves like curl and outputs the last HTTP response's entry. To have a test oriented output, you can use --test option:
$ hurl --test sample.hurl
A particular response can be saved with [Options] section :
GET https://example.ord/cats/123 [Options] output: cat123.txt # use - to output to stdout HTTP 200 GET https://example.ord/dogs/567 HTTP 200
Finally, Hurl can take files as input, or directories. In the latter case, Hurl will search files with .hurl extension recursively.
$ hurl --test integration/ * .hurl $ hurl --test .
You can check Hurl tests suite for more samples.
Getting Data
A simple GET:
GET https://example.org
Requests can be chained:
GET https://example.org/a GET https://example.org/b HEAD https://example.org/c GET https://example.org/c
Doc
HTTP Headers
A simple GET with headers:
GET https://example.org/news User-Agent: Mozilla/5.0 Accept: */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Connection: keep-alive
Doc
Query Params
GET https://example.org/news [Query] order: newest search: something to search count: 100
Or:
GET https://example.org/news?order=newest&search=something%20to%20search&count=100
With [Query] section, params don't need to be URL escaped.
Doc
Basic Authentication
GET https://example.org/protected [BasicAuth] bob: secret
Doc
This is equivalent to construct the request with a Authorization header:
# Authorization header value can be computed with `echo -n 'bob:secret' | base64` GET https://example.org/protected Authorization: Basic Ym9iOnNlY3JldA==
Basic authentication section allows per request authentication. If you want to add basic authentication to all the requests of a Hurl file you could use -u/--user option:
$ hurl --user bob:secret login.hurl
--user option can also be set per request:
GET https://example.org/login [Options] user: bob:secret HTTP 200 GET https://example.org/login [Options] user: alice:secret HTTP 200
Passing Data between Requests
Captures can be used to pass data from one request to another:
POST https://sample.org/orders HTTP 201 [Captures] order_id: jsonpath "$.order.id" GET https://sample.org/orders/{{order_id}} HTTP 200
Doc
Sending Data
Sending HTML Form Data
POST https://example.org/contact [Form] default: false token: {{token}} email: [email protected] number: 33611223344
Doc
Sending Multipart Form Data
POST https://example.org/upload [Multipart] field1: value1 field2: file,example.txt; # One can specify the file content type: field3: file,example.zip; application/zip
Doc
Multipart forms can also be sent with a multiline string body:
POST https://example.org/upload Content-Type: multipart/form-data; boundary="boundary" ``` --boundary Content-Disposition: form-data; name="key1" value1 --boundary Content-Disposition: form-data; name="upload1"; filename="data.txt" Content-Type: text/plain Hello World! --boundary Content-Disposition: form-data; name="upload2"; filename="data.html" Content-Type: text/html {{login}} {{password}} ```
Doc
Using GraphQL Query
A simple GraphQL query:
POST https://example.org/starwars/graphql ```graphql { human(id: "1000") { name height(unit: FOOT) } } ```
A GraphQL query with variables:
POST https://example.org/starwars/graphql ```graphql query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { name friends @include(if: $withFriends) { name } } } variables { "episode": "JEDI", "withFriends": false } ```
GraphQL queries can also use Hurl templates.
Doc
Using Dynamic Datas
Functions like newUuid and newDate can be used in templates to create dynamic datas:
A file that creates a dynamic email (i.e [email protected] ):
POST https://example.org/api/foo { "name": "foo", "email": "{{newUuid}}@test.com" }
A file that creates a dynamic query parameter (i.e 2024-12-02T10:35:44.461731Z ):
GET https://example.org/api/foo [Query] date: {{newDate}} HTTP 200
Doc
Testing Response
Responses are optional, everything after HTTP is part of the response asserts.
# A request with (almost) no check: GET https://foo.com # A status code check: GET https://foo.com HTTP 200 # A test on response body GET https://foo.com HTTP 200 [Asserts] jsonpath "$.state" == "running"
Testing Status Code
GET https://example.org/order/435 HTTP 200
Doc
GET https://example.org/order/435 # Testing status code is in a 200-300 range HTTP * [Asserts] status >= 200 status < 300
Doc
Testing Response Headers
Use implicit response asserts to test header values:
GET https://example.org/index.html HTTP 200 Set-Cookie: theme=light Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT
Doc
Or use explicit response asserts with predicates:
GET https://example.org HTTP 302 [Asserts] header "Location" contains "www.example.net"
Doc
Implicit and explicit asserts can be combined:
GET https://example.org/index.html HTTP 200 Set-Cookie: theme=light Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT [Asserts] header "Location" contains "www.example.net"
Testing REST APIs
Asserting JSON body response (node values, collection count etc...) with JSONPath:
GET https://example.org/order screencapability: low HTTP 200 [Asserts] jsonpath "$.validated" == true jsonpath "$.userInfo.firstName" == "Franck" jsonpath "$.userInfo.lastName" == "Herbert" jsonpath "$.hasDevice" == false jsonpath "$.links" count == 12 jsonpath "$.state" != null jsonpath "$.order" matches "^order-\\d{8}$" jsonpath "$.order" matches /^order-\d{8}$/ # Alternative syntax with regex literal jsonpath "$.created" isIsoDate
Doc
Testing HTML Response
GET https://example.org HTTP 200 Content-Type: text/html; charset=UTF-8 [Asserts] xpath "string(/html/head/title)" contains "Example" # Check title xpath "count(//p)" == 2 # Check the number of p xpath "//p" count == 2 # Similar assert for p xpath "boolean(count(//h2))" == false # Check there is no h2 xpath "//h2" not exists # Similar assert for h2 xpath "string(//div[1])" matches /Hello.*/
Doc
Testing Set-Cookie Attributes
GET https://example.org/home HTTP 200 [Asserts] cookie "JSESSIONID" == "8400BAFE2F66443613DC38AE3D9D6239" cookie "JSESSIONID[Value]" == "8400BAFE2F66443613DC38AE3D9D6239" cookie "JSESSIONID[Expires]" contains "Wed, 13 Jan 2021" cookie "JSESSIONID[Secure]" exists cookie "JSESSIONID[HttpOnly]" exists cookie "JSESSIONID[SameSite]" == "Lax"
Doc
Testing Bytes Content
Check the SHA-256 response body hash:
GET https://example.org/data.tar.gz HTTP 200 [Asserts] sha256 == hex,039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81;
Doc
SSL Certificate
Check the properties of a SSL certificate:
GET https://example.org HTTP 200 [Asserts] certificate "Subject" == "CN=example.org" certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3" certificate "Expire-Date" daysAfterNow > 15 certificate "Serial-Number" matches /[\da-f]+/
Doc
Checking Full Body
Use implicit body to test an exact JSON body match:
GET https://example.org/api/cats/123 HTTP 200 { "name" : "Purrsloud", "species" : "Cat", "favFoods" : ["wet food", "dry food", "any food"], "birthYear" : 2016, "photo" : "https://learnwebcode.github.io/json-example/images/cat-2.jpg" }
Doc
Or an explicit assert file:
GET https://example.org/index.html HTTP 200 [Asserts] body == file,cat.json;
Doc
Implicit asserts supports XML body:
GET https://example.org/api/catalog HTTP 200 Gambardella, Matthew XML Developer's Guide Computer 44.95 2000-10-01 An in-depth look at creating applications with XML.
Doc
Plain text:
GET https://example.org/models HTTP 200 ``` Year,Make,Model,Description,Price 1997,Ford,E350,"ac, abs, moon",3000.00 1999,Chevy,"Venture ""Extended Edition""","",4900.00 1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00 1996,Jeep,Grand Cherokee,"MUST SELL! air, moon roof, loaded",4799.00 ```
Doc
One line:
POST https://example.org/helloworld HTTP 200 `Hello world!`
Doc
File:
GET https://example.org HTTP 200 file,data.bin;
Doc
Reports
HTML Report
$ hurl --test --report-html build/report/ * .hurl
Doc
JSON Report
$ hurl --test --report-json build/report/ * .hurl
Doc
JUnit Report
$ hurl --test --report-junit build/report.xml * .hurl
Doc
TAP Report
$ hurl --test --report-tap build/report.txt * .hurl
Doc
JSON Output
A structured output of running Hurl files can be obtained with --json option. Each file will produce a JSON export of the run.
$ hurl --json * .hurl
Others
HTTP Version
Testing HTTP version (HTTP/1.0, HTTP/1.1, HTTP/2 or HTTP/3) can be done using implicit asserts:
GET https://foo.com HTTP/3 200 GET https://bar.com HTTP/2 200
Doc
Or explicit:
GET https://foo.com HTTP 200 [Asserts] version == "3" GET https://bar.com HTTP 200 [Asserts] version == "2" version toFloat > 1.1
Doc
IP Address
Testing the IP address of the response, as a string. This string may be IPv6 address:
GET https://foo.com HTTP 200 [Asserts] ip == "2001:0db8:85a3:0000:0000:8a2e:0370:733" ip startsWith "2001" ip isIpv6
Polling and Retry
Retry request on any errors (asserts, captures, status code, runtime etc...):
# Create a new job POST https://api.example.org/jobs HTTP 201 [Captures] job_id: jsonpath "$.id" [Asserts] jsonpath "$.state" == "RUNNING" # Pull job status until it is completed GET https://api.example.org/jobs/{{job_id}} [Options] retry: 10 # maximum number of retry, -1 for unlimited retry-interval: 500ms HTTP 200 [Asserts] jsonpath "$.state" == "COMPLETED"
Doc
Delaying Requests
Add delay for every request, or a particular request:
# Delaying this request by 5 seconds (aka sleep) GET https://example.org/turtle [Options] delay: 5s HTTP 200 # No delay! GET https://example.org/turtle HTTP 200
Doc
Skipping Requests
# a, c, d are run, b is skipped GET https://example.org/a GET https://example.org/b [Options] skip: true GET https://example.org/c GET https://example.org/d
Doc
Testing Endpoint Performance
GET https://sample.org/helloworld HTTP * [Asserts] duration < 1000 # Check that response time is less than one second
Doc
Using SOAP APIs
POST https://example.org/InStock Content-Type: application/soap+xml; charset=utf-8 SOAPAction: "http://www.w3.org/2003/05/soap-envelope" GOOG HTTP 200
Doc
Capturing and Using a CSRF Token
GET https://example.org HTTP 200 [Captures] csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)" POST https://example.org/login?user=toto&password=1234 X-CSRF-TOKEN: {{csrf_token}} HTTP 302
Doc
Redacting Secrets
Using command-line for known values:
$ hurl --secret token=1234 file.hurl
POST https://example.org X-Token: {{token}} { "name": "Alice", "value": 100 } HTTP 200
Doc
Using redact for dynamic values:
# Get an authorization token: GET https://example.org/token HTTP 200 [Captures] token: header "X-Token" redact # Send an authorized request: POST https://example.org X-Token: {{token}} { "name": "Alice", "value": 100 } HTTP 200
Doc
Checking Byte Order Mark (BOM) in Response Body
GET https://example.org/data.bin HTTP 200 [Asserts] bytes startsWith hex,efbbbf;
Doc
AWS Signature Version 4 Requests
Generate signed API requests with AWS Signature Version 4, as used by several cloud providers.
POST https://sts.eu-central-1.amazonaws.com/ [Options] aws-sigv4: aws:amz:eu-central-1:sts [Form] Action: GetCallerIdentity Version: 2011-06-15
The Access Key is given per --user , either with command line option or within the [Options] section:
POST https://sts.eu-central-1.amazonaws.com/ [Options] aws-sigv4: aws:amz:eu-central-1:sts user: bob=secret [Form] Action: GetCallerIdentity Version: 2011-06-15
Doc
Using curl Options
curl options (for instance --resolve or --connect-to ) can be used as CLI argument. In this case, they're applicable to each request of an Hurl file.
$ hurl --resolve foo.com:8000:127.0.0.1 foo.hurl
Use [Options] section to configure a specific request:
GET http://bar.com HTTP 200 GET http://foo.com:8000/resolve [Options] resolve: foo.com:8000:127.0.0.1 HTTP 200 `Hello World!`
Doc
Manual
Name
hurl - run and test HTTP requests.
Synopsis
hurl [options] [FILE...]
Description
Hurl is a command line tool that runs HTTP requests defined in a simple plain text format.
It can chain requests, capture values and evaluate queries on headers and body response. Hurl is very versatile, it can be used for fetching data and testing HTTP sessions: HTML content, REST / SOAP / GraphQL APIs, or any other XML / JSON based APIs.
$ hurl session.hurl
If no input files are specified, input is read from stdin.
$ echo GET http://httpbin.org/get | hurl { " args " : {}, " headers " : { " Accept " : " */* " , " Accept-Encoding " : " gzip " , " Content-Length " : " 0 " , " Host " : " httpbin.org " , " User-Agent " : " hurl/0.99.10 " , " X-Amzn-Trace-Id " : " Root=1-5eedf4c7-520814d64e2f9249ea44e0 " }, " origin " : " 1.2.3.4 " , " url " : " http://httpbin.org/get " }
Hurl can take files as input, or directories. In the latter case, Hurl will search files with .hurl extension recursively.
Output goes to stdout by default. To have output go to a file, use the -o, --output option:
$ hurl -o output input.hurl
By default, Hurl executes all HTTP requests and outputs the response body of the last HTTP call.
To have a test oriented output, you can use --test option:
$ hurl --test * .hurl
Hurl File Format
The Hurl file format is fully documented in https://hurl.dev/docs/hurl-file.html
It consists of one or several HTTP requests
GET http://example.org/endpoint1 GET http://example.org/endpoint2
Capturing values
A value from an HTTP response can be-reused for successive HTTP requests.
A typical example occurs with CSRF tokens.
GET https://example.org HTTP 200 # Capture the CSRF token value from html body. [Captures] csrf_token: xpath "normalize-space(//meta[@name='_csrf_token']/@content)" # Do the login ! POST https://example.org/login?user=toto&password=1234 X-CSRF-TOKEN: {{csrf_token}}
More information on captures can be found here https://hurl.dev/docs/capturing-response.html
Asserts
The HTTP response defined in the Hurl file are used to make asserts. Responses are optional.
At the minimum, response includes assert on the HTTP status code.
GET http://example.org HTTP 301
It can also include asserts on the response headers
GET http://example.org HTTP 301 Location: http://www.example.org
Explicit asserts can be included by combining a query and a predicate
GET http://example.org HTTP 301 [Asserts] xpath "string(//title)" == "301 Moved"
With the addition of asserts, Hurl can be used as a testing tool to run scenarios.
More information on asserts can be found here https://hurl.dev/docs/asserting-response.html
Options
Options that exist in curl have exactly the same semantics.
Options specified on the command line are defined for every Hurl file's entry, except if they are tagged as cli-only (can not be defined in the Hurl request [Options] entry)
For instance:
$ hurl --location foo.hurl
will follow redirection for each entry in foo.hurl . You can also define an option only for a particular entry with an [Options] section. For instance, this Hurl file:
GET https://example.org HTTP 301 GET https://example.org [Options] location: true HTTP 200
will follow a redirection only for the second entry.
Environment
Environment variables can only be specified in lowercase.
Using an environment variable to set the proxy has the same effect as using the -x, --proxy option.
Variable Description http_proxy [PROTOCOL://][:PORT] Sets the proxy server to use for HTTP.
https_proxy [PROTOCOL://][:PORT] Sets the proxy server to use for HTTPS.
all_proxy [PROTOCOL://][:PORT] Sets the proxy server to use if no protocol-specific proxy is set.
no_proxy List of host names that shouldn't go through any proxy.
HURL_name value Define variable (name/value) to be used in Hurl templates. This is similar than --variable and --variables-file options.
NO_COLOR When set to a non-empty string, do not colorize output (see --no-color option).
Exit Codes
Value Description 0 Success.
1 Failed to parse command-line options.
2 Input File Parsing Error.
3 Runtime error (such as failure to connect to host).
4 Assert Error.
WWW
https://hurl.dev
See Also
curl(1) hurlfmt(1)
Installation
Binaries Installation
Linux
Precompiled binary (depending on libc >=2.35) is available at Hurl latest GitHub release:
$ INSTALL_DIR=/tmp $ VERSION=6.1.1 $ curl --silent --location https://github.com/Orange-OpenSource/hurl/releases/download/ $VERSION /hurl- $VERSION -x86_64-unknown-linux-gnu.tar.gz | tar xvz -C $INSTALL_DIR $ export PATH= $INSTALL_DIR /hurl- $VERSION -x86_64-unknown-linux-gnu/bin: $PATH
Debian / Ubuntu
For Debian >=12 / Ubuntu >=22.04, Hurl can be installed using a binary .deb file provided in each Hurl release.
$ VERSION=6.1.1 $ curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/ $VERSION /hurl_ ${VERSION} _amd64.deb $ sudo apt update && sudo apt install ./hurl_ ${VERSION} _amd64.deb
For Ubuntu >=18.04, Hurl can be installed from ppa:lepapareil/hurl
$ VERSION=6.1.1 $ sudo apt-add-repository -y ppa:lepapareil/hurl $ sudo apt install hurl= " ${VERSION} " *
Alpine
Hurl is available on testing channel.
$ apk add --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing hurl
Arch Linux / Manjaro
Hurl is available on extra channel.
$ pacman -Sy hurl
NixOS / Nix
NixOS / Nix package is available on stable channel.
macOS
Precompiled binaries for Intel and ARM CPUs are available at Hurl latest GitHub release.
Homebrew
$ brew install hurl
MacPorts
$ sudo port install hurl
FreeBSD
$ sudo pkg install hurl
Windows
Windows requires the Visual C++ Redistributable Package to be installed manually, as this is not included in the installer.
Zip File
Hurl can be installed from a standalone zip file at Hurl latest GitHub release. You will need to update your PATH variable.
Installer
An executable installer is also available at Hurl latest GitHub release.
Chocolatey
$ choco install hurl
Scoop
$ scoop install hurl
Windows Package Manager
$ winget install hurl
Cargo
If you're a Rust programmer, Hurl can be installed with cargo.
$ cargo install --locked hurl
$ conda install -c conda-forge hurl
Hurl can also be installed with conda-forge powered package manager like pixi .
Docker
$ docker pull ghcr.io/orange-opensource/hurl:latest
npm
$ npm install --save-dev @orangeopensource/hurl
Building From Sources
Hurl sources are available in GitHub.
Build on Linux
Hurl depends on libssl, libcurl and libxml2 native libraries. You will need their development files in your platform.
Debian based distributions
$ apt install -y build-essential pkg-config libssl-dev libcurl4-openssl-dev libxml2-dev libclang-dev
Fedora based distributions
$ dnf install -y pkgconf-pkg-config gcc openssl-devel libxml2-devel clang-devel
Red Hat based distributions
$ yum install -y pkg-config gcc openssl-devel libxml2-devel clang-devel
Arch based distributions
$ pacman -S --noconfirm pkgconf gcc glibc openssl libxml2 clang
Alpine based distributions
$ apk add curl-dev gcc libxml2-dev musl-dev openssl-dev clang-dev
Build on macOS
$ xcode-select --install $ brew install pkg-config
Hurl is written in Rust. You should install the latest stable release.
$ curl https://sh.rustup.rs -sSf | sh -s -- -y $ source $HOME /.cargo/env $ rustc --version $ cargo --version
Then build hurl:
$ git clone https://github.com/Orange-OpenSource/hurl $ cd hurl $ cargo build --release $ ./target/release/hurl --version
Build on Windows
Please follow the contrib on Windows section.
Hello World!
--boundary-- ```
In that case, files have to be inlined in the Hurl file.
Doc
Posting a JSON Body
With an inline JSON:
POST https://example.org/api/tests { "id": "456", "evaluate": true }
Doc
With a local file:
POST https://example.org/api/tests Content-Type: application/json file,data.json;
Doc
Templating a JSON Body
PUT https://example.org/api/hits Content-Type: application/json { "key0": "{{a_string}}", "key1": {{a_bool}}, "key2": {{a_null}}, "key3": {{a_number}} }
Variables can be initialized via command line:
$ hurl --variable a_string=apple \ --variable a_bool=true \ --variable a_null=null \ --variable a_number=42 \ test.hurl
Resulting in a PUT request with the following JSON body:
{ "key0": "apple", "key1": true, "key2": null, "key3": 42 }
Doc
Templating a XML Body
Using templates with XML body is not currently supported in Hurl. You can use templates in XML multiline string body with variables to send a variable XML body:
POST https://example.org/echo/post/xml ```xml