ApiCatcher Scripting Feature Guide

ApiCatcher features a built-in JavaScript-based script engine that allows you to write custom scripts to intercept and modify HTTP/HTTPS requests and responses. The scripting capability is powerful and flexible, making it ideal for development debugging, API testing, data mocking, and more.


Table of Contents


Quick Start

  1. Open ApiCatcher and go to the Scripts management page.
  2. Click Add Script; the system will automatically populate template code.
  3. Set the script's Matching Conditions (Host, Path, Method). Matching depends on the App version (3.17 and later: exact / wildcard; earlier: contains / prefix). See Scope.
  4. Write your interception logic.
  5. Once the script is enabled, start packet capture for it to take effect.

Basic Script Structure

Each script can contain two functions:

// Request Interceptor (Optional)
function interceptRequest(request) {
    // Your logic
    return { action: 'passthrough' };
}

// Response Interceptor (Optional)
function interceptResponse(request, response) {
    // Your logic
    return { action: 'passthrough' };
}
  • You can define either one or both of these functions.
  • Scripts are executed in order of priority and creation time.
  • Once a script returns a result other than passthrough, subsequent scripts will not be executed.

Request Interceptor: interceptRequest

The interceptRequest function is called when a matching HTTP request arrives.

Parameter: request object

PropertyTypeDescription
methodstringHTTP method (GET, POST, PUT, DELETE, etc.)
urlstringFull URL
hoststringHostname
portnumberPort number
pathstringRequest path
schemestringProtocol (http / https)
headersobjectRequest headers (key-value object)
bodystringRequest body string
queryParamsobjectURL query parameters (key-value object)

Return Value

Returns an object containing an action field to decide how to handle the request:

1. Pass Through (passthrough)

return { action: 'passthrough' };

Makes no modifications; the request is sent normally.

2. Modify Request (modify)

request.headers['Authorization'] = 'Bearer my-token';
request.body = JSON.stringify({ modified: true });
return { action: 'modify', request: request };

Modifies the request content and proceeds with the delivery.

3. Mock Response (mock)

return {
    action: 'mock',
    response: {
        statusCode: 200,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ success: true, data: 'mocked' })
    }
};

Directly returns a mock response without sending the actual request.

4. Redirect (redirect)

return {
    action: 'redirect',
    url: 'https://new-api.example.com' + request.path,
    statusCode: 302  // Optional, defaults to 302
};

Redirects the request to another URL.

5. Drop Request (drop)

return { action: 'drop' };

Drops the request immediately; it is neither sent nor responded to.


Response Interceptor: interceptResponse

The interceptResponse function is called after the server returns a response.

Parameters

  • request: Original request information (same as above)
  • response: Response information
PropertyTypeDescription
statusCodenumberHTTP status code
headersobjectResponse headers (key-value object)
bodystringResponse body string

Return Value

1. Pass Through (passthrough)

return { action: 'passthrough' };

2. Modify Response (modify)

var data = JSON.parse(response.body);
data.injected = true;
response.body = JSON.stringify(data);
return { action: 'modify', response: response };

3. Delay Response (delay)

return {
    action: 'delay',
    response: response,
    delay: 3000  // Delay in milliseconds (3 seconds)
};

4. Drop Response (drop)

return { action: 'drop' };

Built-in APIs

The following global objects are pre-loaded in the script environment and can be used directly within interceptRequest and interceptResponse.

httpClient — HTTP Requests

Make HTTP/HTTPS requests within your scripts. Calls are synchronous, meaning the script will wait for the request to complete before proceeding. The duration of each request is automatically logged to help troubleshoot performance issues.

Methods

// GET request
httpClient.get(url)
httpClient.get(url, headers)

// POST request
httpClient.post(url)
httpClient.post(url, body)
httpClient.post(url, body, headers)

// PUT request
httpClient.put(url)
httpClient.put(url, body)
httpClient.put(url, body, headers)

// DELETE request
httpClient.delete(url)
httpClient.delete(url, headers)

Parameter Descriptions

ParameterTypeRequiredDescription
urlstring✅Full request URL (starting with http:// or https://)
bodystring❌Request body string (use JSON.stringify() for JSON)
headersobject❌Request headers object

Return Value

All methods return a response object with the same structure:

{
    statusCode: 200,        // HTTP status code (-1 if the request fails)
    headers: { ... },       // Response headers object
    body: "...",            // Response body string
    error: ""              // Error message (empty string if successful)
}

Example

// Simple GET request
var resp = httpClient.get('https://api.example.com/status');
console.log('Status: ' + resp.statusCode);

// GET request with headers
var resp = httpClient.get('https://api.example.com/user', {
    'Authorization': 'Bearer my-token'
});

// POST JSON data
var resp = httpClient.post(
    'https://api.example.com/data',
    JSON.stringify({ name: 'test', value: 123 }),
    { 'Content-Type': 'application/json' }
);

// Check if the request was successful
if (resp.error) {
    console.error('Request failed: ' + resp.error);
} else {
    var data = JSON.parse(resp.body);
    console.log('Response data: ' + JSON.stringify(data));
}

Automatic Latency Logs

After each httpClient call, the engine automatically outputs a log:

[Script] httpClient.get https://api.example.com/status -> 200 (156ms)
[Script] httpClient.post https://api.example.com/data -> 201 (342ms)

You can view this information in the ApiCatcher log panel to help locate slow requests.

⚠️ Note: httpClient calls are synchronous and will block script execution until the HTTP request finishes (timeout is 15 seconds). Making time-consuming HTTP requests in a script may delay the response of the original intercepted request. Please monitor the duration data in the logs.


localStore — Local Storage

Provides simple Key-Value local persistent storage. Data is stored in the App's UserDefaults and is shared across all scripts. If you need to isolate data by Host or other dimensions, please add a prefix to your keys manually (e.g., request.host + '_token').

Methods

// Write data
localStore.write(key, value)

// Read data
localStore.read(key)     // Returns string or null

// Remove data
localStore.remove(key)

Parameter Descriptions

ParameterTypeDescription
keystringThe name of the key to store
valueanyThe value to store (will be automatically converted to a string)

Storage Details

  • All scripts share the same storage space.
  • Data is persisted in the App's sandbox (local storage) and remains available after an App restart.
  • To isolate by Host, include Host information in the key:
// Manual host isolation
var key = request.host + '_token';
localStore.write(key, 'my-token');

Example

// Basic read/write
localStore.write('counter', '1');
var count = localStore.read('counter');   // '1'
console.log('Count: ' + count);

// Store JSON object
var config = { debug: true, maxRetry: 3 };
localStore.write('config', JSON.stringify(config));

// Read JSON object
var raw = localStore.read('config');
if (raw) {
    var config = JSON.parse(raw);
    console.log('Debug mode: ' + config.debug);
}

// Delete data
localStore.remove('counter');
var v = localStore.read('counter');  // null

crypto — Hashing and Encoding

Provides hashing, HMAC, Base64 encoding, and cryptographically secure random values for request signing (OAuth, JWT, custom API signatures, and similar flows).

Methods

// Hash (lowercase hex)
crypto.md5(s)
crypto.sha1(s)
crypto.sha256(s)
crypto.sha512(s)

// HMAC (lowercase hex)
crypto.hmac(s, key, algorithm)   // algorithm defaults to 'sha256'; 'sha1' | 'sha256' | 'sha512'
crypto.hmacSha1(s, key)
crypto.hmacSha256(s, key)
crypto.hmacSha512(s, key)

// Base64
crypto.base64Encode(s)           // UTF-8 string → standard Base64
crypto.base64Decode(s)           // standard Base64 → UTF-8 string (empty string on invalid input)
crypto.base64URLEncode(s)        // UTF-8 string → Base64URL (no padding)
crypto.base64URLEncodeHex(hex)   // hex digest → Base64URL (no padding; for HMAC/hash output, e.g. JWT signature)

// Secure random values
crypto.randomHex(len)            // default length 16
crypto.randomString(len)         // default length 16
crypto.uuid()                    // lowercase UUID

Method Reference

MethodReturnsDescription
crypto.md5(s)hex stringMD5 hash (for signing/checksums, not security)
crypto.sha1(s) / sha256(s) / sha512(s)hex stringSHA hash
crypto.hmac(s, key, algorithm)hex stringHMAC; algorithm defaults to sha256
crypto.hmacSha1(s, key) / hmacSha256 / hmacSha512hex stringHMAC with a fixed algorithm
crypto.base64Encode(s) / base64Decode(s)stringStandard Base64 encode/decode; decode returns "" on failure
crypto.base64URLEncode(s)stringUTF-8 string to Base64URL (no padding)
crypto.base64URLEncodeHex(hex)stringHex digest to Base64URL (no padding), e.g. JWT signature
crypto.randomHex(len) / randomString(len)stringCSPRNG string, default length 16
crypto.uuid()stringLowercase UUID

Example

function interceptRequest(request) {
    var secret = 'your-secret';
    var ts = String(Math.floor(Date.now() / 1000));
    var nonce = crypto.randomHex(16);
    var stringToSign = request.method + '\n' + ts + '\n' + nonce;
    request.headers['X-Timestamp'] = ts;
    request.headers['X-Nonce'] = nonce;
    request.headers['X-Signature'] = crypto.hmacSha256(stringToSign, secret);
    return { action: 'modify', request: request };
}

When minting an HS256 JWT, HMAC returns hex — re-encode it with base64URLEncodeHex:

function interceptRequest(request) {
    var secret = localStore.read('jwt_secret');
    var now = Math.floor(Date.now() / 1000);
    var header = crypto.base64URLEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
    var payload = crypto.base64URLEncode(JSON.stringify({
        iss: localStore.read('jwt_client_id'),
        iat: now,
        exp: now + 3600
    }));
    var signingInput = header + '.' + payload;
    var jwt = signingInput + '.' + crypto.base64URLEncodeHex(crypto.hmacSha256(signingInput, secret));
    request.headers['Authorization'] = 'Bearer ' + jwt;
    return { action: 'modify', request: request };
}

console — Log Output

Used for outputting logs within scripts, which can be viewed in the ApiCatcher log panel.

console.log('General log');       // Info level
console.warn('Warning log');      // Warning level
console.error('Error log');       // Error level

Supports multiple arguments; objects are automatically serialized to JSON:

console.log('Request info:', request.method, request.url);
console.log('Response data:', { status: response.statusCode, body: response.body });

Real-world Examples

Example 1: Automatic Auth Token Injection

Intercept all requests to api.myserver.com and automatically add an Authorization header. The token is fetched from a login API and cached in localStore.

function interceptRequest(request) {
    // Only process the target server
    if (!request.host.includes('api.myserver.com')) {
        return { action: 'passthrough' };
    }
    
    // Attempt to read token from cache
    var token = localStore.read('auth_token');
    var tokenExpiry = localStore.read('auth_token_expiry');
    
    // Check if the token is expired (using seconds timestamp)
    var now = Math.floor(Date.now() / 1000);
    if (!token || !tokenExpiry || now > parseInt(tokenExpiry)) {
        // Token missing or expired, re-authenticate to fetch new one
        console.log('Token expired, re-authenticating...');
        var loginResp = httpClient.post(
            'https://api.myserver.com/auth/login',
            JSON.stringify({ username: 'testuser', password: 'testpass' }),
            { 'Content-Type': 'application/json' }
        );
        
        if (loginResp.statusCode === 200) {
            var loginData = JSON.parse(loginResp.body);
            token = loginData.token;
            // Cache token and set 1-hour expiry
            localStore.write('auth_token', token);
            localStore.write('auth_token_expiry', String(now + 3600));
            console.log('New token cached successfully');
        } else {
            console.error('Login failed: ' + loginResp.statusCode);
            return { action: 'passthrough' };
        }
    }
    
    // Inject Authorization header
    request.headers['Authorization'] = 'Bearer ' + token;
    return { action: 'modify', request: request };
}

Example 2: API Data Forwarding / Log Reporting

Report intercepted request and response information to your log analysis server.

function interceptResponse(request, response) {
    // Only report specific paths
    if (!request.path.startsWith('/api/v2/')) {
        return { action: 'passthrough' };
    }
    
    // Collect request and response info
    var logData = {
        timestamp: new Date().toISOString(),
        request: {
            method: request.method,
            url: request.url,
            headers: request.headers
        },
        response: {
            statusCode: response.statusCode,
            body: response.body
        }
    };
    
    // Report to log server (Note: this is synchronous and blocks the return of the current response)
    var reportResp = httpClient.post(
        'https://log.myserver.com/api/collect',
        JSON.stringify(logData),
        { 'Content-Type': 'application/json' }
    );
    
    if (reportResp.error) {
        console.warn('Log report failed: ' + reportResp.error);
    }
    
    // Pass through the original response
    return { action: 'passthrough' };
}

Example 3: A/B Testing — Dynamic Response Modification

Use a counter for simple A/B split testing, alternately returning different mock data.

function interceptRequest(request) {
    if (request.path !== '/api/recommend') {
        return { action: 'passthrough' };
    }
    
    // Read split counter from localStore
    var count = parseInt(localStore.read('ab_counter') || '0');
    count++;
    localStore.write('ab_counter', String(count));
    
    // Alternately return different versions
    var version = (count % 2 === 0) ? 'A' : 'B';
    
    if (version === 'A') {
        return {
            action: 'mock',
            response: {
                statusCode: 200,
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    version: 'A',
                    items: [{ id: 1, name: 'Recommend A-1' }, { id: 2, name: 'Recommend A-2' }]
                })
            }
        };
    } else {
        return {
            action: 'mock',
            response: {
                statusCode: 200,
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    version: 'B',
                    items: [{ id: 3, name: 'Recommend B-1' }, { id: 4, name: 'Recommend B-2' }]
                })
            }
        };
    }
}

Example 4: Rate Limiting Protection

Record the request frequency for the same API and return a 429 error when a threshold is exceeded.

function interceptRequest(request) {
    if (!request.path.startsWith('/api/')) {
        return { action: 'passthrough' };
    }
    
    var key = 'rate_' + request.path;
    var now = Math.floor(Date.now() / 1000);
    
    // Read timestamp and count of the last request
    var rawData = localStore.read(key);
    var data = rawData ? JSON.parse(rawData) : { count: 0, windowStart: now };
    
    // Reset counter if the time window exceeds 60 seconds
    if (now - data.windowStart > 60) {
        data = { count: 0, windowStart: now };
    }
    
    data.count++;
    localStore.write(key, JSON.stringify(data));
    
    // Max 10 requests per minute
    if (data.count > 10) {
        console.warn('Rate limit exceeded for ' + request.path);
        return {
            action: 'mock',
            response: {
                statusCode: 429,
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    error: 'Too Many Requests',
                    retryAfter: 60 - (now - data.windowStart)
                })
            }
        };
    }
    
    return { action: 'passthrough' };
}

Variables and Scope

Top-level variables and helpers

Variables and helper functions declared outside interceptRequest / interceptResponse are available inside the corresponding entry function. A common pattern is to keep constants and utility functions at the top level:

var secret = 'my-key';

function sign(body) {
    return crypto.hmacSha256(body, secret);
}

function interceptRequest(request) {
    request.headers['X-Sign'] = sign(request.body || '');
    return { action: 'modify', request: request };
}

Request and response phases

Request and response interception run separately. Changes to top-level variables in interceptRequest are not visible in interceptResponse for the same HTTP transaction; on the next matching request, top-level variables are initialized again:

var token = null;

function interceptRequest(request) {
    token = request.headers['Authorization'];
    return { action: 'passthrough' };
}

function interceptResponse(request, response) {
    console.log(token);  // ❌ Still null — cannot read the value set above
    return { action: 'passthrough' };
}

Use localStore to pass data between request and response, or to persist state across requests:

function interceptRequest(request) {
    var auth = request.headers['Authorization'];
    if (auth) {
        localStore.write(request.host + '_auth', auth);
    }
    return { action: 'passthrough' };
}

function interceptResponse(request, response) {
    var auth = localStore.read(request.host + '_auth');
    console.log('auth from request: ' + auth);
    return { action: 'passthrough' };
}

Syntax limitations

  • Declare variables with var / let / const; avoid bare assignments such as cache = {}
  • Top-level variables cannot be used as a cross-request cache
  • Entry functions must be named interceptRequest and interceptResponse; you may define only one
  • async/await, fetch, import/export, and setTimeout are not supported; use httpClient for HTTP

Considerations

  1. httpClient is Synchronous: HTTP requests initiated in the script will block script execution until they complete or timeout (15 seconds). This impacts the processing speed of the original request. Please monitor the request duration automatically provided in the logs.

  2. Shared localStore: All scripts share the same storage space. If you need to isolate data by Host, manually add a prefix to your keys.

  3. Execution Order: Multiple scripts are executed based on their priority. Once a script returns a result other than passthrough, subsequent scripts will be skipped.

  4. Remote Scripts: Loading scripts from a URL is supported. The remote script content is automatically fetched when packet capture starts. If the fetch fails, the script will be skipped.

  5. JSON Handling: Both request and response bodies are strings. When dealing with JSON data, use JSON.parse() for parsing and JSON.stringify() for serialization.

  6. Error Handling: It is recommended to use try-catch blocks in critical logic and log exceptions using console.error() to prevent a script crash from breaking the interception chain.

  7. Utility Functions: The engine comes with two pre-defined utility functions:

    • safeJsonParse(str) — Safely parses JSON; returns null if it fails.
    • deepClone(obj) — Performs a deep clone of an object.