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
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 {{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.