Skip to content
HTTPS Capture ManualCapture Scripts

HTTPS Capture Scripts: JavaScript Hooks for Requests and Responses

The complete reference for HTTPS Capture's JavaScript rule scripts: script matching, the five hook points, the request/response APIs, header and query bags, shared state, persistent environment variables, limits and safety rules.

6 min read

When rewrite rules are too rigid for the job, Scripts give you a real programming language inside the capture engine. A script is a plain JavaScript file that HTTPS Capture runs at five well-defined points of each request/response exchange — you can inspect and rewrite URLs, headers, queries and bodies, share state across requests, keep counters in persistent storage, and block traffic outright.

Manage scripts at Rules → Scripts. Each script has an enable toggle; drag to reorder — the first enabled script whose match applies handles a request exclusively, so order is priority. The built-in editor provides syntax highlighting, and you can import .capture.js files, share scripts out via the share button, or start from one of the bundled examples. The AI Prompt page copies a complete API specification you can paste into any AI assistant to generate a valid script; Runtime Logs shows console output from running scripts; Script Env lists persistent environment variables.

A first script

Capture.defineScript({
  name: "Debug Header",
  version: "1.0.0",
  description: "Adds a debug header to API requests.",
  match: {
    urls: ["https://api.example.com/*"],
    excludeUrls: [],
    methods: []
  },

  onRequestHeaders(context, request) {
    request.headers.set("X-Debug", "1");
    return request;
  }
});

A file must call Capture.defineScript({...}) exactly once, and the editor validates it before saving: the name must be set, match.urls must contain at least one non-empty entry, and the source must parse. Everything else — hooks, description, version — is optional.

Matching

  • match.urls — required. Each entry is a glob with * wildcards, matched against the full request URL.
  • match.excludeUrls — takes precedence over urls.
  • match.methods — case-insensitive list (["GET", "POST"]); an empty array matches every method.
  • shouldMatch(request) — optional function hook that runs after static matching for final say, e.g. skip health checks with return request.path !== "/health".

Only the first enabled script whose match applies runs for a request; other scripts do not see it. Reordering the list changes which script wins.

The five hooks

Include only the hooks you need; all of them may be async.

HookRuns whenTypical use
shouldMatch(request)After static URL/method matching, before any other hookFine-grained matching decisions
onRequestHeaders(context, request)Request headers parsed, before the upstream connection opensRewrite URL, host, path, queries, headers
onRequest(context, request)Request body fully receivedInspect or replace the request body
onResponseHeaders(context, request, response)Response headers received, before they are delivered to the clientRewrite response headers and status
onResponse(context, request, response)Response body fully bufferedInspect or replace the response body

Every hook returns the (possibly modified) request or response object. Returning null from a request hook (onRequestHeaders or onRequest) blocks the request — the connection is dropped before anything reaches the server.

Request API

request.id           // Capture record ID
request.originalUrl  // Original absolute URL; treat as read-only
request.url          // Absolute HTTP/HTTPS URL, readable and writable
request.method       // HTTP method, readable and writable
request.host         // Hostname, readable and writable
request.port         // Number or null, readable and writable
request.path         // URL path, readable and writable
request.queries      // Ordered query bag (see below)
request.headers      // Ordered, case-insensitive header bag (see below)
request.body         // UTF-8 string or null; assigning replaces body bytes
request.bodyBytes    // Uint8Array copy; assigning replaces body bytes

To change routing, assign a valid absolute request.url, or update host, port and path individually — the connection is re-resolved after the header hooks. Query mutations rebuild request.url automatically.

Response API

response.statusCode  // Integer from 100 through 599
response.headers     // Ordered, case-insensitive header bag
response.body        // UTF-8 string or null; assigning replaces body bytes
response.bodyBytes   // Uint8Array copy; assigning replaces body bytes

After a body change, HTTPS Capture recalculates body framing (Content-Length, removes stale Content-Encoding) for you. Set Content-Type yourself when you change the representation format.

Header and query bags

Both bags preserve duplicate values (several Set-Cookie headers, repeated query parameters) and support the same operations:

bag.get(name)          // First value or undefined
bag.getAll(name)       // All values as an array
bag.set(name, value)   // Replace all values with one value
bag.append(name, value)
bag.delete(name)
bag.toJSON()           // Array of { name, value } pairs

Property syntax works too:

request.headers["X-Debug"] = "1";
delete request.headers["X-Remove"];
request.queries["mode"] = "debug";

Header names are case-insensitive; query names are case-sensitive. Use append when duplicates must be preserved.

State: per-flow and per-session

context.state   // JSON object shared between hooks for this request/response flow
context.shared  // JSON object shared across all flows this script handles, cleared when capture stops

A typical pattern: stamp a start time in onRequest, compute the elapsed milliseconds in onResponse. Keep values JSON-compatible (null, booleans, numbers, strings, arrays, plain objects) and small — each object is limited to 64 KB. Never store credentials or body payloads in shared state.

Persistent environment variables

env.get(key)        // Stored string, or null when missing
env.set(key, value) // Persists a string across runs, scripts and app restarts

env survives capture restarts and is shared with the JavaScript requests feature in the Requests tab; browse stored values under Rules → Scripts → Script Env. Use it for tokens or counters that must outlive a single flow. Limits: keys 1–128 characters, values up to 4,096 characters, at most 100 keys / 256 KB total. Reads inside a hook see a snapshot taken when the hook started; writes made before an exception still persist.

Built-in utilities and logging

Capture.crypto.md5(text)
Capture.crypto.sha256(text)
Capture.base64.encode(text)
Capture.base64.decode(base64Text)

console.log(...values)
console.info(...values)
console.warn(...values)
console.error(...values)

Console output lands in Rules → Scripts → Runtime Logs — bounded to 100 entries per execution and 2,048 characters each, so keep messages short and never log authorization values, cookies or tokens.

Limits and safety rules

  • Script source is limited to 256 KB.
  • Body hooks (onRequest, onResponse) process at most 4 MB; larger bodies and streaming responses (Server-Sent Events) skip body hooks.
  • Time budgets: matching 50 ms, header hooks 100 ms, body hooks 1 second. Overrunning kills and rebuilds the runtime.
  • Requests must stay absolute HTTP/HTTPS URLs with ports 1–65535; header names must be valid HTTP tokens and values must not contain NUL, CR or LF — invalid output is rejected, protecting both the client connection and the upstream.
  • There is no network, file or process access: fetch, XMLHttpRequest, WebSocket and EventSource are removed from the runtime, and scripts run in an isolated content world.
  • Scripts fail open: if a script throws or times out, the traffic continues with the last valid data and an error entry appears in Runtime Logs. A buggy script can slow debugging but cannot take your device offline.

Prefer header hooks when you do not need body access, avoid unbounded loops and repeated JSON.parse, and preserve duplicate headers and queries unless replacement is the point.

A larger example

Capture.defineScript({
  name: "Block Ads, Time API",
  version: "1.0.0",
  description: "Blocks ad hosts and injects elapsed time into API responses.",
  match: {
    urls: ["*://*/*"],
    excludeUrls: [],
    methods: []
  },

  onRequestHeaders(context, request) {
    if (request.host.endsWith("ads.example.com")) return null; // block
    return request;
  },

  onRequest(context, request) {
    context.state.startedAt = Date.now();
    return request;
  },

  onResponse(context, request, response) {
    if (request.headers.get("X-Debug") === "1" && response.body) {
      try {
        const body = JSON.parse(response.body);
        body._elapsedMs = Date.now() - (context.state.startedAt || Date.now());
        response.body = JSON.stringify(body);
      } catch (e) {
        console.warn("non-JSON body skipped");
      }
    }
    response.headers.delete("X-Internal-Debug");
    return response;
  }
});

Where to go next

  • Rules — the rule types that surround scripts: rewrite, mock, routing, replay, breakpoints and network simulation.
  • Quick Start — capturing your first session with the local VPN and CA certificate.