npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

http-client-runner

v2.0.0

Published

Execute JetBrains-style .http files programmatically with axios — supports request chaining, JS handlers, variables, imports, and environments

Readme

http-client-runner

npm version license

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.

Installation

npm install http-client-runner

Global install (for CLI usage):

npm install -g http-client-runner

Quick Start

import { 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');

CLI

Run .http files directly from the terminal:

http-client-runner api-tests.http --env development --verbose

Options:

| 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 failed

API

runFile(filePath, options?): Promise<RunResult>

Executes 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) |

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;
}

runString(content, options?): Promise<RunResult>

Same as runFile but accepts the .http content as a string. Accepts an additional baseDir option for resolving relative file paths.

parseHttpFile(filePath): RequestDescriptor[]

Low-level parser that returns an array of request descriptors without executing them. Filters out import/run directives.

parseHttpString(content, baseDir?): RequestDescriptor[]

Same as parseHttpFile but takes a string.

parseHttpFileEntries(filePath): ParsedEntry[]

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.

parseHttpStringEntries(content, baseDir?): ParsedEntry[]

Same as parseHttpFileEntries but takes a string.

HttpClientRunner

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 });

Exported Types

All interfaces are exported from the package for use in your TypeScript code:

import type {
  RunResult,
  RunOptions,
  RunSummary,
  RequestResult,
  RequestDescriptor,
  TestResult,
  IHttpResponse,
  HttpClientRunnerRunnerOptions,
  ParsedEntry,
  ImportDirective,
  RunDirective,
  CacheAdapter,
  CachedResponse,
  CacheDirective,
} from 'http-client-runner';

.http File Format

The library supports the JetBrains HTTP Client file format.

Basic Request

GET https://httpbin.org/get
Accept: application/json

Request with Body

POST https://httpbin.org/post
Content-Type: application/json

{
  "name": "test",
  "value": 42
}

Multiple Requests (separated by ###)

GET https://httpbin.org/get

###

POST https://httpbin.org/post
Content-Type: application/json

{"key": "value"}

Named Requests

### 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/login

Variables

Use {{variable}} syntax anywhere in URLs, headers, or bodies:

GET {{host}}/api/users
Authorization: Bearer {{token}}

Built-in Dynamic Variables

| Variable | Description | |---|---| | {{$uuid}} | Random UUID | | {{$timestamp}} | Unix timestamp (seconds) | | {{$isoTimestamp}} | ISO 8601 timestamp | | {{$randomInt}} | Random integer 0-999 |

Response Handlers (JavaScript)

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 code
  • response.body — parsed response body
  • response.contentType.mimeType — content type string
  • response.headers.valueOf(name) — single header value
  • response.headers.valuesOf(name) — array of header values
  • client.global.set(name, value) — store a variable for subsequent requests
  • client.global.get(name) — read a stored variable
  • client.global.clear(name) — remove a variable
  • client.global.clearAll() — remove all variables
  • client.test(name, fn) — define a test assertion
  • client.assert(condition, message) — assert a condition
  • client.log(text) — log output
  • client.exit() — stop handler execution

Pre-request Scripts

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}}"}

Chaining Requests

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");
    });
%}

Tests and Assertions

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.

Directives

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) |

Response Caching

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/config

Named 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/health

Only 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 });

Response Redirect to File

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.

Body from File

POST https://api.example.com/upload
Content-Type: application/json

< ./payload.json

External Handler Scripts

GET https://api.example.com/data
> ./scripts/handle-response.js

Importing and Running Requests from Other Files

You can split your .http files into reusable modules and compose them using import and run directives.

import — Load requests for later use

Place import at the top of your .http file to make the named requests from another file available:

import auth.http
import helpers.http

run — Execute requests

Run a named request from an imported file:

import auth.http

### Authenticate first
run #Login

Run 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)

How state flows

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}}

Full import/run example

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

Entry-level parser API

If 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;
  }
}

Environment Files

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.

Full Example

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`);

Security

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.

Development

git clone <repo-url>
cd http-client-runner
npm install
npm run build        # Compile TypeScript → dist/
npm test             # Run example .http files against httpbin.org

License

CC BY-NC 4.0 — Free for non-commercial use. For commercial licensing, contact the author.