Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

httpsuite

Run .http files as API tests, for local development and CI.

httpsuite executes the requests in your .http files — the same ones you author and debug in the JetBrains HTTP Client (GoLand, IntelliJ) or the VS Code REST Client — runs their pre-request and response-handler scripts, checks client.test assertions and declarative # @expect checks, and reports pass/fail with the right output for where it's running: coloured and aligned on a terminal, plain and diffable in CI, or JUnit XML for a dashboard.

It ships as a single static binary with no CGO and no JVM. The JavaScript for scripts is executed by the pure-Go goja engine, so the same binary runs unchanged on macOS, Windows, and any Linux distro — including Alpine/musl and FROM scratch/distroless containers. That footprint is the main reason to reach for httpsuite over JetBrains' own ijhttp CLI, which requires a JDK.

Verified compatible. httpsuite's behaviour is checked against the real JetBrains HTTP Client CLI: a conformance harness runs the same .http files through both tools and diffs their JUnit output test-by-test. On the shared .http surface the two agree — and httpsuite is a superset in several places (see Compatibility).

Install

go install github.com/uradical/httpsuite@latest

Requires Go 1.26 or later (see go.mod). Released binaries are static and carry no toolchain requirement.

Usage

httpsuite [--var key=value]... [--env name] [--report file] [--version] [path]
  • path is optional and defaults to the current directory.
  • --var key=value overrides a {{placeholder}}. Repeatable.
  • --env name selects an environment from http-client.env.json (see Variables).
  • --report file writes a JUnit XML report.
  • --version prints version information and exits.
httpsuite                          # discover and run in the current directory
httpsuite ./api                    # run a directory
httpsuite ./api/users.http         # run a single file
httpsuite --var token=abc ./api    # override {{token}}
httpsuite --env prod ./api         # use the "prod" environment
httpsuite --report report.xml ./api

Exit code is 0 when everything passes, 1 when any request, assertion, or script test fails, and 2 on a usage or setup error.

Discovery

When path is a directory (or omitted), httpsuite decides what to run:

  1. If a httpsuite.yaml exists in the directory, it is used as the suite definition.
  2. Otherwise every *.http file in the directory is run serially, in sorted order.

When path is a single file, only that file is run.

Suite definition (httpsuite.yaml)

parallel: true          # run groups concurrently (default false)
timeout: 30s            # global per-request timeout (default 10s)
groups:
  - name: auth
    serial: true        # opt this group out of parallel execution
    files:
      - auth/login.http
      - auth/refresh.http
  - name: users
    timeout: 5s         # override the timeout for this group
    files:
      - users/list.http
      - users/create.http
  • Files within a group always run serially, in order.
  • serial: true keeps a group off the parallel path even when parallel is on.
  • timeout may be set globally and overridden per group.

Shared session and setup

Every group runs against one shared session: a single cookie jar, one client.global, and one response store are threaded through all of its files. So a login in the first file leaves its session cookie in place for the rest, and a token stashed with client.global.set(...) — or a named response referenced as {{login.response.body.$.token}} — is visible to every later file in the group.

setup: files run first, in the same session, for programmatic setup such as authentication. They are ordinary .http files (still portable to GoLand); their results are included in the report so a failed setup surfaces.

groups:
  - name: account
    setup:
      - auth/login.http        # sets a session cookie / stores a token
    files:
      - account/profile.http   # runs already logged in
      - account/update.http

With data: rows, each row gets its own fresh session, so setup re-runs per row.

Data-driven runs and retries

Orchestration that would make a .http file non-portable lives here in the YAML — the .http files stay plain and still run in GoLand. The suite file adds two things on top of them:

groups:
  - name: users
    files: [users/get.http]     # uses {{id}} and {{name}}
    data:                       # run the file once per row; keys become {{variables}}
      - { id: "1", name: "Ada" }
      - { id: "2", name: "Bob" }
    # dataFile: users/cases.json  # …or an external JSON array / CSV table

  - name: jobs
    files: [jobs/poll.http]     # a request named "# @name pollStatus"
    retry:                      # re-run a named request until it passes
      - request: pollStatus     # matches "# @name pollStatus"
        attempts: 20
        delay: 2s
  • Data-driven: each data row (or dataFile record) runs the group's files once, with the row's key/values available as {{placeholders}} (highest precedence). dataFile accepts a JSON array of objects or a CSV with a header row.
  • Retry / until: a named request is re-run until its own assertions pass (# @expect or client.test) or attempts is reached, waiting delay between tries — so a poll is just a normal request carrying # @expect body.status == "done", with the polling policy kept out of the file.

The .http format

httpsuite parses the full portable .http syntax:

### Create a user            # text after ### names the request

# @name createUser           # or name it explicitly
POST {{base}}/users
    ?notify=true             # multi-line URL: indented continuations are appended
    &source=cli
Content-Type: application/json
Authorization: Bearer
    {{token}}                # folded header value (joined with a space)
...commonHeaders             # spread an object variable's entries as headers

{ "name": "Ada" }
Feature Notes
Request line [METHOD] URL [version] Method is optional and defaults to GET; the HTTP version is accepted and ignored.
### separators Trailing text becomes the request name (unless # @name is given).
Comments #, //, and /* … */ block comments.
Multi-line URLs Indented continuation lines are concatenated onto the URL.
Folded header values An indented line continues the previous header's value (RFC 7230, joined with a space).
Spread headers ...objectVar injects an object variable's key/value pairs as headers.
Request body Everything after the blank line.
Body from a file < ./body.json imports the file (path and contents are variable-substituted).
Form bodies application/x-www-form-urlencoded lines split with a leading & are joined (a=1\n&b=2a=1&b=2).
Metadata tags # @name, # @no-cookie-jar, # @no-redirect.

Cookies and redirects

Requests share a cookie jar: a response's Set-Cookie is automatically sent on later requests (matching domain/path). A standalone file run has its own jar; within a suite the jar is shared across a whole group, so a login in a setup: file is carried into the group's requests (see Shared session). Redirects are followed by default. Opt out per request:

# @no-cookie-jar             # send no stored cookies; store none from this response
# @no-redirect               # return a 3xx instead of following it
GET {{base}}/whoami

Variables

{{placeholder}} tokens in a request's URL, headers, and body are resolved at execution time. Resolution order, from lowest to highest precedence:

  1. JetBrains environment files, when --env is given (see below), including $shared
  2. File-level @key = value (eager) and @key := value (lazy) declarations
  3. OS environment variables
  4. --var key=value flags and data-driven data/dataFile row values
  5. Values set by scripts via client.global.set(...)

A plain {{placeholder}} that is still unresolved at execution time fails that request.

Eager vs lazy declarations

@id  = {{$uuid}}    # eager: evaluated once, the same value for the whole file
@rid := {{$uuid}}   # lazy:  re-evaluated on each use (a new value every time)
@url = {{base}}/api # eager values may reference other variables

Dynamic variables

Generated fresh on each use, matching the JetBrains HTTP Client:

Variable Value
{{$uuid}} / {{$guid}} a random v4 UUID
{{$timestamp}} current Unix time in seconds
{{$isoTimestamp}} current time, ISO-8601 UTC
{{$randomInt}} / {{$randomInt min max}} a random integer (default 0–1000)
{{$env.NAME}} / {{$processEnv NAME}} an OS environment variable
{{%name}} URL-encodes the value of name

Request chaining

Reference a previous request's response, either with a response-handler script (the JetBrains way — client.global.set(...), works everywhere), or in place:

# @name login
POST {{base}}/login

###
GET {{base}}/users/{{login.response.body.$.id}}
Authorization: Bearer {{login.response.headers.X-Token}}

Supported in-place forms: {{name.response.body.$.json.path}} (with [index]), {{name.response.headers.Name}}, {{name.response.status}}, and {{name.response.body}}.

JetBrains environment files

With --env <name>, httpsuite loads variables from http-client.env.json and http-client.private.env.json sitting next to the .http file. The private file overrides the public one, and a $shared environment applies to all environments:

// http-client.env.json
{ "$shared": { "apiVersion": "v2" },
  "dev":     { "baseUrl": "http://localhost:8080" },
  "prod":    { "baseUrl": "https://api.example.com" } }

// http-client.private.env.json
{ "dev":  { "token": "dev-secret" },
  "prod": { "token": "prod-secret" } }
httpsuite --env prod ./api      # {{baseUrl}}, {{token}}, {{apiVersion}} resolve

These are the same files GoLand uses, so an environment set up there works unchanged — httpsuite discovers them automatically (the ijhttp CLI instead requires explicit -v/-p paths). An object-valued env entry can also be used as a spread header.

Scripts

httpsuite runs the JetBrains HTTP Client scripting subset. Scripts are plain JavaScript (ES2015+; goja provides the language). Two kinds are supported:

  • Pre-request scripts run before the request is sent, introduced with <:

    < {% request.variables.set("nonce", Date.now().toString()) %}
    POST {{base}}/orders
    
    ### or from an external file, resolved relative to the .http file
    < ./scripts/sign.js
    GET {{base}}/secure
  • Response-handler scripts run after the response arrives, introduced with >. They typically register tests:

    GET {{base}}/users/1
    
    > {%
      client.test("status is 200", () => {
        client.assert(response.status === 200, "got " + response.status);
      });
      client.global.set("userName", response.body.name);   // reuse in later requests
    %}
    
    ### or from an external file
    GET {{base}}/users/2
    > ./scripts/check-user.js

Script body statements run first; then every client.test(...) block runs in registration order (matching JetBrains). A failing client.assert marks that one test failed and the remaining tests still run.

Available API

Object Highlights
client test(name, fn), assert(cond, msg), log(...), exit(), and client.global (set/get/isEmpty/clear/clearAll, plus global.headers.set/clear to inject headers into later requests). client.global persists across every request in a run.
response status, contentType.{mimeType,charset}, headers.valueOf(name) / valuesOf(name), cookies() / cookiesByName(name), and body — a parsed object for JSON, a DOM Document for XML/HTML, or a raw string otherwise (based on Content-Type).
request Pre-request: method, url/body (getRaw/tryGetSubstituted), headers, environment.get(name), variables.get/set. Response: method, url(), body(), headers.
crypto crypto.hmac.{sha256,sha384,sha512,sha3}, and a synchronous crypto.subtle (digest, generateKey, importKey, exportKey, sign, verify) for RSA-PSS / ECDSA / HMAC.
jwt sign(payload, secret, {algorithm}), verify(token, secret), decode(token) — HS/RS/PS/ES 256/384/512.
helpers jsonPath(body, "$.a.b[0]"), xpath(doc, "//tag"), console.log, btoa/atob, URLSearchParams, structuredClone, string2byteArray, DOMParser/XMLSerializer.

A request fails if there is a network error or timeout, any # @expect assertion fails, any client.test has a failing client.assert, or a pre-request or response script throws an uncaught error. A pre-request error skips the HTTP call.

Not supported: async/await/Promise resolution (scripts must be synchronous), ES module import/export, and streaming response handlers. Shell execution (exec/execSync/spawn) is deliberately blocked.

Assertions (# @expect)

For simple checks you don't need a script — attach # @expect comments to a request. All assertions on a request are evaluated (evaluation never stops at the first failure) and a request fails if any fails. A request with no assertions and no failing tests passes as long as it returns any HTTP response. # @expect is an httpsuite convenience and is ignored by other .http tools.

# @name createUser
POST {{baseUrl}}/users
Content-Type: application/json
# @expect status == 201
# @expect header Location exists
# @expect body.id number
# @expect body.name == "Ada Lovelace"
# @expect body.roles length > 0
# @expect duration < 500

{ "name": "Ada Lovelace" }

Status

# @expect status == 201        # == != < > <= >=
# @expect status 2xx           # 2xx 3xx 4xx 5xx range match

Headers

# @expect header X-Request-Id exists
# @expect header Content-Type == application/json   # case-insensitive contains

Body

The response body is parsed as JSON. Paths use dot notation with array indexing, e.g. body.items[0].id.

# @expect body.id exists
# @expect body.id == 10                 # numeric compare: == != < > <= >=
# @expect body.name == "Alan Bradley"   # quoted string
# @expect body.active == true           # boolean
# @expect body.items[0].name == "Alan"  # array indexing

# type checks
# @expect body.count number             # integer, no decimal point
# @expect body.price double             # number with a decimal point
# @expect body.name string
# @expect body.active boolean
# @expect body.address object
# @expect body.items array

# length of an array or string
# @expect body.items length 3
# @expect body.items length > 0

# dates
# @expect body.created date                        # ISO 8601 by default
# @expect body.created date "YYYY-MM-DD"
# @expect body.timestamp date "YYYY-MM-DD HH:mm"

Date format tokens: YYYY MM DD (date) and HH mm ss (time).

Duration

# @expect duration < 500        # milliseconds elapsed for the request

Output

On a terminal, results are coloured (green pass, red fail, bold summary) with / marks on assertion and test lines, for script logs. With no TTY (CI), the same layout is printed without ANSI codes and with PASS/FAIL/LOG words instead of glyphs, and --report writes the same results as JUnit XML:

PASS  POST  https://api.example.com/login     200  89ms
  PASS  Status is 200
  PASS  Token extracted
FAIL  GET   https://api.example.com/profile   401  12ms
  PASS  Request executed
  FAIL  Profile returned      expected status 200, got 401
  LOG   checking auth header value "Bearer undefined"

SCRIPT ERROR  GET  https://api.example.com/orders
  TypeError: Cannot read property 'id' of undefined

3 requests  1 passed  2 failed  190ms

Compatibility

The conformance/ harness runs the same .http files through httpsuite and the real JetBrains ijhttp CLI and diffs their JUnit reports test-by-test. On the shared surface the two agree (CONFORMANT) — covering status/body/global state, headers, environments, the full ES scripting API, cookies, and dynamic variables.

httpsuite is a superset of the ijhttp CLI. These are valid .http features (from VS Code REST Client / httpyac) that stock ijhttp does not support, and which httpsuite handles: block comments /* */, folded header values, spread headers ...var, in-place response references, and lazy @var := declarations.

crypto and jwt match the JetBrains IDE's scripting API but are not defined in the stock ijhttp CLI, so scripts relying on them run in httpsuite but not under ijhttp. (xpath, URLSearchParams, structuredClone, and string2byteArray are provided by httpsuite but not yet verified against ijhttp.)

Out of scope: non-HTTP protocols (WebSocket, gRPC, MQTT, …), cloud auth helpers (OAuth/AAD — also absent from the ijhttp CLI), and interactive prompts.

Development

go test ./...     # unit tests
go vet ./...

# conformance vs the JetBrains CLI (needs ijhttp on PATH, or pass --ijhttp <path>)
go run ./conformance

Building a release binary is CGO-free and cross-compiles to every supported target:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" .

About

JetBrains HTTP Client compatible test runner for CI pipelines

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages