Execute JetBrains-style .http files programmatically with axios — supports request chaining, JS handlers, variables, imports, environments, and response caching.
Use it as a library in your Node.js/TypeScript code, or as a CLI to run .http files directly from the terminal.
npm install http-client-runnerGlobal install (for CLI usage):
npm install -g http-client-runnerimport { runFile } from 'http-client-runner';
const { results, summary } = await runFile('./api-tests.http', {
environment: 'development',
verbose: true,
});
console.log(summary);
// { totalRequests: 3, executedRequests: 3, passedTests: 5, failedTests: 0, ... }CommonJS is also supported:
const { runFile } = require('http-client-runner');Run .http files directly from the terminal:
http-client-runner api-tests.http --env development --verboseOptions:
| Flag | Description |
|---|---|
--env, -e <name> |
Environment name (from http-client.env.json) |
--var, -v <key=value> |
Set a variable (repeatable) |
--verbose |
Print request/response details |
--help, -h |
Show help |
Examples:
# Run with an environment
http-client-runner api.http --env staging
# Pass variables
http-client-runner tests.http -v host=https://api.example.com -v token=abc123
# Run multiple files sequentially (state carries across files)
http-client-runner auth.http api-tests.http --verbose
# Compact output (no --verbose)
http-client-runner api.http -v host=https://httpbin.org
# ✓ Login — 200
# ✓ Get Users — 200
# ✓ Status is 200
# ✓ Returns array
# Requests: 2 | Tests: 2 passed, 0 failedExecutes all requests in a .http file sequentially.
Options (RunOptions):
| Option | Type | Description |
|---|---|---|
environment |
string |
Environment name to load from http-client.env.json |
variables |
Record<string, string> |
Additional variables to inject (e.g. { host: 'http://localhost:3000' }) |
verbose |
boolean |
Print request/response info to stdout |
client |
HttpClientRunner |
Reuse an existing client instance (shares global variables across runs) |
requestFn |
RequestFn |
Custom HTTP transport — replaces the built-in axios call. Falls back to axios if omitted. |
Returns RunResult:
interface RunResult {
results: RequestResult[];
summary: RunSummary;
client: HttpClientRunner;
}
interface RunSummary {
totalRequests: number;
executedRequests: number;
skippedRequests: number;
totalTests: number;
passedTests: number;
failedTests: number;
}
interface RequestResult {
name: string;
request: { method: string; url: string };
status: number | null;
response: IHttpResponse | null;
testResults: TestResult[];
logs: string[];
skipped: boolean;
}Same as runFile but accepts the .http content as a string. Accepts an additional baseDir option for resolving relative file paths.
Low-level parser that returns an array of request descriptors without executing them. Filters out import/run directives.
Same as parseHttpFile but takes a string.
Full parser that returns all entries including import/run directives alongside request descriptors. Use this when you need to process the complete structure of an .http file.
Same as parseHttpFileEntries but takes a string.
Create a standalone client to share state across multiple file runs:
import { HttpClientRunner, runFile } from 'http-client-runner';
const client = new HttpClientRunner({ verbose: true });
// Run auth file first – sets tokens in client.global
await runFile('./auth.http', { client });
// Run API tests – reuses auth tokens
await runFile('./api-tests.http', { client });Inject a custom HTTP transport via requestFn. The executor calls it instead of the built-in axios call — useful for adding structured logging, auth strategies, or instrumentation from your existing HTTP layer:
import { HttpClientRunner, runFile } from 'http-client-runner';
import type { RequestFn } from 'http-client-runner';
const client = new HttpClientRunner();
const variables = { host: 'https://api.example.com' };
// Example: wrap your own HTTP client for logging/auth
const requestFn: RequestFn = (config) => myHttpClient.request(config);
await runFile('./vendor.http', {
client,
variables,
requestFn,
});Falls back to the built-in axios call when requestFn is omitted — fully backward-compatible.
All interfaces are exported from the package for use in your TypeScript code:
import type {
RunResult,
RunOptions,
RunSummary,
RequestResult,
RequestDescriptor,
TestResult,
IHttpResponse,
HttpClientRunnerOptions,
ParsedEntry,
ImportDirective,
RunDirective,
CacheAdapter,
CachedResponse,
CacheDirective,
RequestFn,
} from 'http-client-runner';The library supports the JetBrains HTTP Client file format.
GET https://httpbin.org/get
Accept: application/jsonPOST https://httpbin.org/post
Content-Type: application/json
{
"name": "test",
"value": 42
}GET https://httpbin.org/get
###
POST https://httpbin.org/post
Content-Type: application/json
{"key": "value"}### Login request
POST https://api.example.com/login
Content-Type: application/json
{"username": "admin", "password": "secret"}Or with @name directive:
# @name LoginRequest
POST https://api.example.com/loginUse {{variable}} syntax anywhere in URLs, headers, or bodies:
GET {{host}}/api/users
Authorization: Bearer {{token}}| Variable | Description |
|---|---|
{{$uuid}} |
Random UUID |
{{$timestamp}} |
Unix timestamp (seconds) |
{{$isoTimestamp}} |
ISO 8601 timestamp |
{{$randomInt}} |
Random integer 0-999 |
Inline handler after a request — runs after the response is received:
POST https://api.example.com/login
Content-Type: application/json
{"username": "admin", "password": "secret"}
> {%
client.global.set("authToken", response.body.token);
client.log("Got token: " + response.body.token);
%}The handler has access to:
response.status— HTTP status coderesponse.body— parsed response bodyresponse.contentType.mimeType— content type stringresponse.headers.valueOf(name)— single header valueresponse.headers.valuesOf(name)— array of header valuesclient.global.set(name, value)— store a variable for subsequent requestsclient.global.get(name)— read a stored variableclient.global.clear(name)— remove a variableclient.global.clearAll()— remove all variablesclient.test(name, fn)— define a test assertionclient.assert(condition, message)— assert a conditionclient.log(text)— log outputclient.exit()— stop handler execution
Run JavaScript before a request is sent:
< {%
request.variables.set("timestamp", Date.now().toString());
%}
POST https://api.example.com/events
Content-Type: application/json
{"time": "{{timestamp}}"}The key feature — pass data from one request to the next:
### Step 1: Login
POST {{host}}/auth/login
Content-Type: application/json
{"username": "admin", "password": "secret"}
> {%
client.global.set("token", response.body.token);
client.global.set("userId", response.body.user.id);
%}
### Step 2: Get user profile (uses token from step 1)
GET {{host}}/users/{{userId}}
Authorization: Bearer {{token}}
> {%
client.test("Status is 200", function() {
client.assert(response.status === 200, "Expected 200");
});
client.test("Has correct user", function() {
client.assert(response.body.id == client.global.get("userId"), "User ID mismatch");
});
%}GET https://httpbin.org/get
> {%
client.test("Status is 200", function() {
client.assert(response.status === 200, "Expected 200 OK");
});
client.test("Content type is JSON", function() {
client.assert(
response.contentType.mimeType === "application/json",
"Expected JSON response"
);
});
%}Test results are available in the returned results[].testResults array.
Add directives as comments before a request:
# @no-redirect
# @no-cookie-jar
# @timeout 5000 ms
GET https://api.example.com/slow-endpoint| Directive | Description |
|---|---|
@no-redirect |
Don't follow 3xx redirects |
@no-log |
Exclude from logging |
@no-cookie-jar |
Don't store cookies |
@timeout <value> <unit> |
Read timeout (ms, s, m) |
@connection-timeout <value> <unit> |
Connection timeout |
@cache(ttl=<ms>) |
Cache the response for the given TTL (milliseconds) |
@cache(ttl=<ms>, key=<name>) |
Cache with an explicit key (useful for invalidation) |
Add a @cache directive to avoid repeated network calls for the same request. Cached responses are stored in memory by default and expire after the specified TTL.
Auto-keyed cache — the cache key is derived from method, URL, headers, and body:
# @cache(ttl=30000)
GET {{host}}/api/configNamed cache key — use an explicit key so you can reference or invalidate it:
# @cache(ttl=30000, key=user-profile)
GET {{host}}/api/users/{{userId}}When a cache hit occurs, the stored response is returned without making a network request. Post-response handlers and response redirects still run on cached responses, so your test assertions and variable extraction work the same way.
Cache invalidation from scripts — use client.cache.delete(key) to remove a specific entry or client.cache.clear() to wipe the entire cache. These work in both pre-request and post-response handlers, and are safely awaited even with async adapters like Redis.
Invalidate before a request — force a fresh fetch by deleting the cached entry in a pre-request script:
# @cache(ttl=30000, key=user-profile)
GET {{host}}/api/users/{{userId}}
< {%
// Force a fresh request by clearing the cached entry
client.cache.delete("user-profile");
%}Invalidate after a response — useful when a cached token or session is rejected by the server. The post-response handler can detect the failure and evict the stale entry so the next request fetches a fresh one:
### Cache the auth token for 10 minutes
# @cache(ttl=600000, key=auth-token)
POST {{host}}/auth/token
Content-Type: application/json
{"client_id": "{{clientId}}", "client_secret": "{{clientSecret}}"}
> {%
client.global.set("token", response.body.access_token);
%}
###
### Call a protected endpoint using the cached token
GET {{host}}/api/protected
Authorization: Bearer {{token}}
> {%
if (response.status === 401) {
// Token was revoked or expired server-side — invalidate the cache
// so the next run fetches a fresh token
client.cache.delete("auth-token");
client.log("Auth token invalidated — will refresh on next run");
}
%}To clear all cached entries at once (e.g. after a deployment or environment switch):
< {%
client.cache.clear();
%}
GET {{host}}/api/healthOnly successful responses (2xx) are cached. Network errors and non-2xx responses are never stored.
Custom cache adapter — provide your own CacheAdapter implementation (e.g. Redis, filesystem) via the HttpClientRunner constructor:
import { HttpClientRunner, runFile } from 'http-client-runner';
import type { CacheAdapter } from 'http-client-runner';
const redisCache: CacheAdapter = {
async get(key) { /* ... */ },
async set(key, value, ttlMs) { /* ... */ },
async delete(key) { /* ... */ },
async clear() { /* ... */ },
};
const client = new HttpClientRunner({ cacheAdapter: redisCache });
await runFile('./api.http', { client });Save response body to a file:
GET https://api.example.com/data
>> ./output/response.json
GET https://api.example.com/data
>>! ./output/response.json>> creates a new file (adds numeric suffix if exists), >>! overwrites.
POST https://api.example.com/upload
Content-Type: application/json
< ./payload.jsonGET https://api.example.com/data
> ./scripts/handle-response.jsYou can split your .http files into reusable modules and compose them using import and run directives.
Place import at the top of your .http file to make the named requests from another file available:
import auth.http
import helpers.httpRun a named request from an imported file:
import auth.http
### Authenticate first
run #LoginRun a named request with variable overrides using (@key=value) syntax:
import helpers.http
### Create a widget
run #Create Resource (@resourceName=my-widget)
### Create another with two overrides
run #Create Resource (@resourceName=gadget, @owner=alice)Run all requests from a file (no import needed):
### Run the full auth flow
run ./auth.http
### Run helpers with overrides
run ./helpers.http (@host=https://staging.example.com)Variables set via client.global.set() in imported/run request handlers are available to subsequent requests, just like inline requests. This makes it easy to compose flows:
import auth.http
### Step 1 — run Login from auth.http, which sets {{authToken}}
run #Login
### Step 2 — use the token it set
GET {{host}}/api/protected
Authorization: Bearer {{authToken}}auth.http:
### Login
POST {{host}}/auth/login
Content-Type: application/json
{"username": "admin", "password": "secret"}
> {%
client.global.set("token", response.body.token);
%}api-tests.http:
import auth.http
### Authenticate
run #Login
### Use the token from Login
GET {{host}}/api/users
Authorization: Bearer {{token}}
> {%
client.test("Returns users", function() {
client.assert(response.status === 200, "Expected 200");
});
%}
### Run the full helpers suite at the end
run ./helpers.httpIf you need access to import/run directives programmatically:
import { parseHttpFileEntries } from 'http-client-runner';
import type { ParsedEntry } from 'http-client-runner';
const entries: ParsedEntry[] = parseHttpFileEntries('./api-tests.http');
for (const entry of entries) {
switch (entry.kind) {
case 'import':
console.log('Import:', entry.filePath);
break;
case 'run':
console.log('Run:', entry.requestName ?? entry.filePath);
break;
case 'request':
console.log('Request:', entry.descriptor.method, entry.descriptor.url);
break;
}
}Create http-client.env.json in the same directory as your .http file:
{
"development": {
"host": "http://localhost:3000",
"token": "dev-token-123"
},
"staging": {
"host": "https://staging.api.example.com",
"token": "staging-token-456"
},
"production": {
"host": "https://api.example.com",
"token": "prod-token-789"
}
}For secrets, use http-client.private.env.json (add to .gitignore). Private values override public ones.
await runFile('./api.http', { environment: 'development' });Variables from the environment file are resolved in {{variable}} placeholders. Process environment variables (process.env) are also available as fallback.
import { runFile } from 'http-client-runner';
const { results, summary } = await runFile('./examples/chain.http', {
environment: 'development',
variables: { host: 'https://httpbin.org' },
verbose: true,
});
for (const r of results) {
console.log(`${r.name}: ${r.status}`);
for (const t of r.testResults) {
console.log(` ${t.passed ? '✓' : '✗'} ${t.name}${t.error ? ' — ' + t.error : ''}`);
}
}
console.log(`\nTests: ${summary.passedTests} passed, ${summary.failedTests} failed`);Treat .http files with JavaScript handlers as executable code. Response handlers (> {% %}) and pre-request scripts (< {% %}) run inside a Node.js vm context, which is not a security sandbox. Malicious scripts can escape the VM and access the host process.
This is consistent with how JetBrains' own HTTP Client works — handlers run in-process with full trust. Only run .http files you trust, the same way you would only run scripts you trust.
git clone <repo-url>
cd http-client-runner
npm install
npm run build # Compile TypeScript → dist/
npm test # Run example .http files against httpbin.orgCC BY-NC 4.0 — Free for non-commercial use. For commercial licensing, contact the author.