HTTP request recording and replay system for API testing
—
Replay's extensible proxy system allows custom request processing through a configurable chain of handlers. The system processes HTTP requests through a series of proxies until one provides a response.
Add custom proxy handlers to the processing chain.
/**
* Add a proxy handler to the beginning of the processing chain
* @param {ProxyHandler} proxy - Handler function to process requests
* @returns {Replay} The Replay instance for chaining
*/
Replay.use(proxy: ProxyHandler): Replay;
/**
* Proxy handler function type
* @typedef {Function} ProxyHandler
* @param {any} request - The HTTP request object
* @param {Function} callback - Callback function to call with response or error
*/
type ProxyHandler = (request: any, callback: (error?: Error, response?: any) => void) => void;Usage Examples:
const Replay = require('replay');
// Custom authentication proxy
Replay.use(function authProxy(request, callback) {
if (request.url.hostname === 'api.example.com') {
// Add authentication headers
request.headers['Authorization'] = 'Bearer ' + process.env.API_TOKEN;
}
// Pass to next proxy in chain
callback();
});
// Custom response modification proxy
Replay.use(function responseModifier(request, callback) {
if (request.url.pathname === '/api/time') {
// Provide mock response
callback(null, {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timestamp: Date.now() })
});
return;
}
// Pass to next proxy
callback();
});
// Chain multiple proxies
Replay
.use(customLoggingProxy)
.use(authenticationProxy)
.use(mockDataProxy);Replay provides a built-in logging proxy for debugging HTTP requests.
/**
* Create a logging proxy that logs HTTP requests when DEBUG=replay
* @returns {ProxyHandler} Logging proxy handler function
*/
Replay.logger(): ProxyHandler;Usage Examples:
const Replay = require('replay');
// Add logging to the proxy chain
Replay.use(Replay.logger());
// Logger output when DEBUG=replay is set:
// Requesting GET http://api.example.com:80/users
// Requesting GET https://cdn.example.com:443/assets/style.cssEnvironment Configuration:
# Enable debug logging
DEBUG=replay node app.js
# Logger will output request URLs:
# Requesting GET http://api.example.com:80/data
# Requesting GET https://auth.example.com:443/loginDirect access to the internal proxy chain for advanced management.
/**
* Append a handler to the end of the proxy chain (executed last)
* @param {ProxyHandler} handler - Handler function to append
* @returns {Chain} The Chain instance for chaining
*/
Replay.chain.append(handler: ProxyHandler): Chain;
/**
* Prepend a handler to the beginning of the proxy chain (executed first)
* @param {ProxyHandler} handler - Handler function to prepend
* @returns {Chain} The Chain instance for chaining
*/
Replay.chain.prepend(handler: ProxyHandler): Chain;
/**
* Clear all handlers from the proxy chain
*/
Replay.chain.clear(): void;
/**
* Get the first handler in the proxy chain
* @type {ProxyHandler | null}
*/
Replay.chain.start: ProxyHandler | null;Usage Examples:
const Replay = require('replay');
// Direct chain manipulation
Replay.chain.prepend(function firstHandler(request, callback) {
console.log('First handler - always executes first');
callback(); // Pass to next handler
});
Replay.chain.append(function lastHandler(request, callback) {
console.log('Last handler - executes if no other handler provides response');
callback(); // Pass to next handler
});
// Get the first handler
const firstHandler = Replay.chain.start;
if (firstHandler) {
console.log('Chain has handlers');
}
// Clear all handlers (advanced usage - removes built-in functionality)
// Note: This will disable all replay functionality including recording/playback
Replay.chain.clear();
// Chain operations can be chained together
Replay.chain
.prepend(authHandler)
.prepend(loggingHandler)
.append(fallbackHandler);Note: Direct chain manipulation is advanced functionality. The Replay.use() method is recommended for most use cases as it maintains the proper order with built-in handlers.
Replay sets up a default proxy chain:
// Default chain (from first to last):
Replay
.use(passThrough(passWhenBloodyOrCheat)) // Pass-through for bloody/cheat modes
.use(recorder(replay)) // Record/replay functionality
.use(logger(replay)) // Debug logging (when enabled)
.use(passThrough(passToLocalhost)); // Pass-through for localhost/**
* Example custom proxy implementation
*/
function customProxy(request, callback) {
// Check if this proxy should handle the request
if (shouldHandle(request)) {
// Process the request
processRequest(request, function(error, response) {
if (error) {
// Pass error to callback
callback(error);
} else {
// Provide response (terminates chain)
callback(null, response);
}
});
} else {
// Pass to next proxy in chain (no arguments = continue)
callback();
}
}
function shouldHandle(request) {
return request.url.hostname === 'mock.example.com';
}
function processRequest(request, callback) {
// Custom request processing logic
const response = {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mocked: true })
};
callback(null, response);
}
// Add to proxy chain
Replay.use(customProxy);// Proxy that only handles specific hosts
Replay.use(function conditionalProxy(request, callback) {
const hostname = request.url.hostname;
if (hostname === 'api.example.com') {
// Handle API requests specially
handleApiRequest(request, callback);
} else if (hostname.endsWith('.amazonaws.com')) {
// Handle AWS requests
handleAwsRequest(request, callback);
} else {
// Pass to next proxy
callback();
}
});// Proxy that transforms responses
Replay.use(function responseTransformer(request, callback) {
if (request.url.pathname.startsWith('/api/v1/')) {
// Let other proxies handle the request first
callback();
} else {
callback();
}
});
// Note: Response transformation typically requires wrapping other proxies
// or intercepting at a different level in the HTTP stack// Proxy with comprehensive error handling
Replay.use(function errorHandlingProxy(request, callback) {
try {
if (request.url.hostname === 'flaky-api.example.com') {
// Simulate flaky service
if (Math.random() < 0.3) {
const error = new Error('Service temporarily unavailable');
error.code = 'ECONNREFUSED';
callback(error);
return;
}
}
// Pass to next proxy
callback();
} catch (error) {
callback(error);
}
});// Proxy that modifies requests
Replay.use(function requestModifier(request, callback) {
// Add custom headers
request.headers['X-Client-Version'] = '1.2.3';
request.headers['X-Request-ID'] = generateRequestId();
// Modify URL for testing
if (process.env.NODE_ENV === 'test') {
request.url.hostname = request.url.hostname.replace('.com', '.test');
}
// Pass modified request to next proxy
callback();
});
function generateRequestId() {
return Math.random().toString(36).substr(2, 9);
}Proxies are processed in the order they are added with use(). Later proxies are added to the beginning of the chain:
Replay.use(proxyA); // Will execute third
Replay.use(proxyB); // Will execute second
Replay.use(proxyC); // Will execute first
// Execution order: proxyC -> proxyB -> proxyA -> default chainThe use() method returns the Replay instance for method chaining:
Replay
.use(authenticationProxy)
.use(loggingProxy)
.use(mockDataProxy)
.passThrough('*.amazonaws.com')
.drop('*.doubleclick.net');Custom proxies work alongside Replay's built-in functionality:
// Custom proxy + built-in features
Replay
.use(customAuthProxy) // Custom authentication
.use(Replay.logger()) // Built-in logging
.passThrough('cdn.example.com') // Built-in pass-through
.localhost('*.local'); // Built-in localhost routingInstall with Tessl CLI
npx tessl i tessl/npm-replay