Package Exports
- @applitools/req
- @applitools/req/package.json
Readme
@applitools/req
A powerful, flexible HTTP request library with advanced features like retry logic, hooks, fallbacks, and timeout management.
Table of Contents
Installation
yarn add @applitools/reqBasic Usage
import {req} from '@applitools/req'
// Simple GET request
const response = await req('https://api.example.com/data')
const data = await response.json()API Reference
req()
The main function for making HTTP requests.
Signature:
req(input: string | URL | Request, ...options: Options[]): Promise<Response>Parameters:
input- URL string, URL object, or Request objectoptions- One or more option objects that will be merged (optional)
Returns: Promise that resolves to a Response object
makeReq()
Creates a req function with predefined base options.
Signature:
makeReq<TOptions>(baseOptions: Partial<TOptions>): Req<TOptions>Example:
const apiReq = makeReq({
baseUrl: 'https://api.example.com',
headers: {'Authorization': 'Bearer token123'}
})
// All requests will use the base options
const response = await apiReq('/users')Options
All available options for configuring requests:
baseUrl
Type: string
Base URL for relative paths. Automatically adds trailing slash if missing.
Example:
await req('./users', {baseUrl: 'https://api.example.com/v1'})
// Makes request to: https://api.example.com/v1/usersmethod
Type: string
HTTP method (uppercase). Overrides method from Request object.
Example:
await req('https://api.example.com/users', {method: 'POST'})query
Type: Record<string, string | boolean | number | undefined>
Query parameters to append to URL. Merges with existing query params. Undefined values are ignored.
Example:
await req('https://api.example.com/search?page=1', {
query: {
limit: 10,
filter: 'active',
skip: undefined // This won't be added
}
})
// URL: https://api.example.com/search?page=1&limit=10&filter=activeheaders
Type: Record<string, string | string[] | undefined>
HTTP headers. Merges with headers from Request object. Undefined values are filtered out.
Example:
await req('https://api.example.com/data', {
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json',
'X-Optional': undefined // Won't be sent
}
})body
Type: NodeJS.ReadableStream | ArrayBufferView | string | Record<string, any> | any[] | null
Request body. Plain objects and arrays are automatically serialized to JSON with appropriate content-type header.
Example:
// Automatic JSON serialization
await req('https://api.example.com/users', {
method: 'POST',
body: {name: 'John', age: 30}
})
// Binary data
await req('https://api.example.com/upload', {
method: 'POST',
body: Buffer.from('binary data')
})proxy
Type: Proxy | ((url: URL) => Proxy | undefined)
Proxy configuration. Can be an object or function that returns proxy settings based on URL.
Example:
// Static proxy
await req('https://api.example.com/data', {
proxy: {
url: 'http://proxy.company.com:8080',
username: 'user',
password: 'pass'
}
})
// Dynamic proxy
await req('https://api.example.com/data', {
proxy: (url) => {
if (url.hostname.includes('internal')) {
return {url: 'http://internal-proxy:8080'}
}
}
})useDnsCache
Type: boolean
Enable DNS caching for improved performance.
Example:
await req('https://api.example.com/data', {useDnsCache: true})connectionTimeout
Type: number
Total timeout in milliseconds for the entire connection including all retries. Once exceeded, throws ConnectionTimeoutError.
Example:
await req('https://api.example.com/data', {
connectionTimeout: 30000, // 30 seconds total
retry: {statuses: [500]}
})requestTimeout
Type: number | {base: number; perByte: number}
Timeout for a single request in milliseconds. Can be dynamic based on request body size.
Example:
// Fixed timeout
await req('https://api.example.com/data', {
requestTimeout: 5000 // 5 seconds per request
})
// Dynamic timeout based on body size
await req('https://api.example.com/upload', {
method: 'POST',
body: largeBuffer,
requestTimeout: {
base: 1000, // 1 second base
perByte: 0.001 // + 1ms per byte
}
})retryTimeout
Type: number
Maximum duration in milliseconds for all retry attempts across all retry strategies. Once exceeded, throws RetryTimeoutError.
Example:
await req('https://api.example.com/data', {
retryTimeout: 30000, // Stop all retries after 30 seconds total
retry: [
{statuses: [500], timeout: 1000},
{codes: ['ECONNRESET'], timeout: 500}
]
})retry
Type: Retry | Retry[]
Retry configuration(s). Multiple retry configs can be provided as array.
See Retry Logic for detailed examples.
hooks
Type: Hooks | Hooks[]
Lifecycle hooks for request interception and modification.
See Hooks for detailed examples.
fallbacks
Type: Fallback | Fallback[]
Fallback strategies for handling failures.
See Fallbacks for detailed examples.
keepAliveOptions
Type: {keepAlive: boolean; keepAliveMsecs?: number}
HTTP agent keep-alive configuration.
Example:
await req('https://api.example.com/data', {
keepAliveOptions: {
keepAlive: true,
keepAliveMsecs: 1000
}
})signal
Type: AbortSignal
Abort signal for canceling requests.
Example:
const controller = new AbortController()
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000)
await req('https://api.example.com/data', {
signal: controller.signal
})Advanced Features
Retry Logic
The retry option enables automatic retrying of failed requests based on various conditions.
Retry Configuration
interface Retry {
limit?: number // Max retry attempts (default: unlimited if undefined)
timeout?: number | number[] // Delay between retries in ms (default: 0 - no delay)
statuses?: number[] // HTTP status codes to retry (default: none)
codes?: string[] // Error codes to retry (default: none)
validate?: (options) => boolean // Custom validation function (default: undefined)
}Default Behavior:
limit: If undefined or not set, retries will continue indefinitely (use with caution - combine withconnectionTimeoutorretryTimeout)timeout: If undefined, retries happen immediately with no delay (0ms)statuses: If undefined, no status codes trigger retries (must be explicitly configured)codes: If undefined, no error codes trigger retries (must be explicitly configured)validate: If undefined, onlystatusesandcodesare checked- Retry trigger: At least one of
statuses,codes, orvalidatemust match for a retry to occur Retry-Afterheader: If present in the response, overrides the configuredtimeoutvalue
Examples
Retry on specific status codes:
await req('https://api.example.com/data', {
retry: {
statuses: [500, 502, 503], // Retry on server errors
limit: 3, // Max 3 attempts
timeout: 1000 // Wait 1 second between retries
}
})Retry on network errors:
await req('https://api.example.com/data', {
retry: {
codes: ['ECONNRESET', 'ETIMEDOUT'],
limit: 5,
timeout: 2000
}
})Exponential backoff:
await req('https://api.example.com/data', {
retry: {
statuses: [429, 500],
limit: 4,
timeout: [1000, 2000, 4000, 8000] // Double delay each time
}
})Custom validation:
await req('https://api.example.com/data', {
retry: {
validate: async ({response, error}) => {
if (error) return true
if (response?.status === 429) {
// Check rate limit header
return response.headers.has('Retry-After')
}
return false
},
limit: 3
}
})Multiple retry strategies:
await req('https://api.example.com/data', {
retry: [
// Retry network errors quickly
{codes: ['ECONNRESET'], limit: 3, timeout: 500},
// Retry server errors with longer delay
{statuses: [500, 503], limit: 2, timeout: 2000}
]
})Retry with timeout limit:
await req('https://api.example.com/data', {
retryTimeout: 10000, // Stop all retries after 10 seconds
retry: [
{codes: ['ECONNRESET'], timeout: 500},
{statuses: [500, 503], timeout: 1000}
]
})
// If retries take longer than 10 seconds total, RetryTimeoutError is thrownHooks
Hooks allow you to intercept and modify requests/responses at various lifecycle stages.
Available Hooks
interface Hooks {
afterOptionsMerged?(options): TOptions | void
beforeRequest?(options): Request | void
beforeRetry?(options): Request | Stop | void
afterResponse?(options): Response | void
afterError?(options): Error | void
unknownBodyType?(options): void
}Examples
Add authentication header:
await req('https://api.example.com/data', {
hooks: {
beforeRequest: ({request}) => {
request.headers.set('Authorization', `Bearer ${getToken()}`)
}
}
})Log all requests:
await req('https://api.example.com/data', {
hooks: {
beforeRequest: ({request}) => {
console.log(`${request.method} ${request.url}`)
},
afterResponse: ({response}) => {
console.log(`Response: ${response.status}`)
}
}
})Conditional retry prevention:
import {stop} from '@applitools/req'
await req('https://api.example.com/data', {
retry: {statuses: [500]},
hooks: {
beforeRetry: async ({response, attempt, stop}) => {
const data = await response?.json()
if (data?.error === 'FATAL') {
console.log('Fatal error, stopping retries')
return stop
}
console.log(`Retry attempt ${attempt}`)
}
}
})Transform response:
await req('https://api.example.com/data', {
hooks: {
afterResponse: async ({response}) => {
if (!response.ok) {
const error = await response.text()
throw new Error(`API Error: ${error}`)
}
}
}
})Modify request before retry:
await req('https://api.example.com/data', {
retry: {statuses: [401]},
hooks: {
beforeRetry: async ({request, attempt}) => {
// Refresh token on 401
const newToken = await refreshAuthToken()
request.headers.set('Authorization', `Bearer ${newToken}`)
return request
}
}
})Multiple hooks:
await req('https://api.example.com/data', {
hooks: [
{
beforeRequest: ({request}) => {
request.headers.set('X-Request-ID', generateId())
}
},
{
beforeRequest: ({request}) => {
request.headers.set('X-Timestamp', Date.now().toString())
}
}
]
})Fallbacks
Fallbacks provide alternative strategies when requests fail.
Fallback Configuration
interface Fallback {
shouldFallbackCondition: (options) => boolean | Promise<boolean>
updateOptions?: (options) => Options | Promise<Options>
cache?: Map<string, boolean>
}Examples
Fallback to different endpoint:
await req('https://api-primary.example.com/data', {
fallbacks: {
shouldFallbackCondition: ({response}) => response.status >= 500,
updateOptions: ({options}) => ({
...options,
baseUrl: 'https://api-backup.example.com'
})
}
})Try with authentication if unauthorized:
await req('https://api.example.com/data', {
fallbacks: {
shouldFallbackCondition: ({response}) => response.status === 401,
updateOptions: async ({options}) => ({
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${await getToken()}`
}
})
}
})Multiple fallback strategies:
await req('https://api.example.com/data', {
fallbacks: [
// First try: enable keep-alive
{
shouldFallbackCondition: ({response}) => response.status === 503,
updateOptions: ({options}) => ({
...options,
keepAliveOptions: {keepAlive: true}
})
},
// Second try: use proxy
{
shouldFallbackCondition: ({response}) => response.status === 403,
updateOptions: ({options}) => ({
...options,
proxy: {url: 'http://proxy.example.com:8080'}
})
}
]
})Timeouts
Three types of timeouts control request timing:
connectionTimeout
Total timeout across all retries and delays. Throws ConnectionTimeoutError when exceeded.
await req('https://api.example.com/data', {
connectionTimeout: 30000, // 30 seconds total for all attempts
retry: {
statuses: [500],
limit: 5,
timeout: 2000
}
})
// If all 5 retries take too long, ConnectionTimeoutError is thrownrequestTimeout
Timeout for each individual request attempt. Throws RequestTimeoutError when exceeded.
await req('https://api.example.com/data', {
requestTimeout: 5000, // Each attempt times out after 5 seconds
retry: {
codes: ['RequestTimeout'],
limit: 3
}
})retryTimeout
Total timeout for all retry attempts across all retry strategies. Throws RetryTimeoutError when exceeded.
await req('https://api.example.com/data', {
retryTimeout: 15000, // Stop retrying after 15 seconds total
retry: [
{codes: ['ECONNRESET'], timeout: 1000},
{statuses: [500, 503], timeout: 2000}
]
})
// If retry process takes longer than 15 seconds, RetryTimeoutError is thrownDifference between timeouts:
connectionTimeout: Covers the entire request lifecycle including initial attempt and all retriesretryTimeout: Only covers retry attempts (excludes the initial request)requestTimeout: Applies to each individual request attempt
Dynamic request timeout based on body size:
await req('https://api.example.com/upload', {
method: 'POST',
body: fileBuffer,
requestTimeout: {
base: 5000, // 5 seconds baseline
perByte: 0.01 // + 10ms per byte
}
})
// For 1MB file: timeout = 5000 + (1048576 * 0.01) ≈ 15.5 secondsChaining Options
Multiple option objects can be passed and will be deeply merged. This enables powerful composition patterns.
Merge Behavior
- Simple properties (strings, numbers) are overridden
- Objects (
query,headers) are merged - Arrays (
retry,hooks,fallbacks) are concatenated - Later options take precedence
Examples
Basic chaining:
const baseOptions = {
baseUrl: 'https://api.example.com',
headers: {'User-Agent': 'MyApp/1.0'}
}
const authOptions = {
headers: {'Authorization': 'Bearer token'}
}
// Merged result:
// - baseUrl: 'https://api.example.com'
// - headers: {'User-Agent': 'MyApp/1.0', 'Authorization': 'Bearer token'}
await req('/users', baseOptions, authOptions)Override with precedence:
const options1 = {
requestTimeout: 5000,
headers: {'X-Version': '1'}
}
const options2 = {
requestTimeout: 10000,
headers: {'X-Version': '2', 'X-New': 'value'}
}
// Result:
// - requestTimeout: 10000 (overridden)
// - headers: {'X-Version': '2', 'X-New': 'value'} (merged)
await req('https://api.example.com/data', options1, options2)Combining retry strategies:
const networkRetry = {
retry: {codes: ['ECONNRESET'], limit: 3, timeout: 500}
}
const serverRetry = {
retry: {statuses: [500, 503], limit: 2, timeout: 2000}
}
// Both retry strategies will be active
await req('https://api.example.com/data', networkRetry, serverRetry)Accumulating hooks:
const loggingHooks = {
hooks: {
beforeRequest: ({request}) => console.log('Request:', request.url)
}
}
const authHooks = {
hooks: {
beforeRequest: ({request}) => request.headers.set('Auth', 'token')
}
}
// Both hooks execute in order
await req('https://api.example.com/data', loggingHooks, authHooks)Practical composition pattern:
// Define reusable option sets
const apiDefaults = {
baseUrl: 'https://api.example.com',
connectionTimeout: 30000,
retry: {codes: ['ECONNRESET'], limit: 3}
}
const withAuth = {
headers: {'Authorization': `Bearer ${token}`}
}
const withRetry = {
retry: {statuses: [500, 502, 503], limit: 5, timeout: [1000, 2000, 4000]}
}
const withLogging = {
hooks: {
beforeRequest: ({request}) => logger.info('Request', request.url),
afterResponse: ({response}) => logger.info('Response', response.status)
}
}
// Compose as needed
await req('/users', apiDefaults, withAuth)
await req('/critical-data', apiDefaults, withAuth, withRetry, withLogging)Using makeReq for composition:
// Create base client
const apiClient = makeReq({
baseUrl: 'https://api.example.com',
headers: {'User-Agent': 'MyApp/1.0'},
retry: {codes: ['ECONNRESET']}
})
// Add authentication per request
await apiClient('/public-data')
await apiClient('/private-data', {
headers: {'Authorization': 'Bearer token'}
})
// Create specialized client with additional options
const authedClient = makeReq({
baseUrl: 'https://api.example.com',
headers: {
'User-Agent': 'MyApp/1.0',
'Authorization': 'Bearer token'
}
})
await authedClient('/user/profile')Examples
Complete real-world example
import {req, makeReq, stop} from '@applitools/req'
// Create API client with defaults
const apiClient = makeReq({
baseUrl: 'https://api.example.com/v1',
connectionTimeout: 60000,
requestTimeout: 10000,
headers: {
'User-Agent': 'MyApp/2.0',
'Accept': 'application/json'
},
retry: [
// Quick retry for network errors
{
codes: ['ECONNRESET', 'ETIMEDOUT'],
limit: 3,
timeout: 500
},
// Slower retry for server errors
{
statuses: [500, 502, 503],
limit: 5,
timeout: [1000, 2000, 4000, 8000]
}
],
hooks: {
beforeRequest: ({request}) => {
console.log(`→ ${request.method} ${request.url}`)
},
afterResponse: ({response}) => {
console.log(`← ${response.status} ${response.statusText}`)
},
afterError: ({error}) => {
console.error('Request failed:', error.message)
}
}
})
// Authenticated requests
const authedClient = makeReq({
baseUrl: 'https://api.example.com/v1',
headers: {'Authorization': `Bearer ${getToken()}`},
retry: {
statuses: [401],
limit: 1
},
hooks: {
beforeRetry: async ({request, response, stop}) => {
if (response?.status === 401) {
try {
const newToken = await refreshToken()
request.headers.set('Authorization', `Bearer ${newToken}`)
return request
} catch {
return stop
}
}
}
}
})
// Usage
const users = await apiClient('/users', {
query: {page: 1, limit: 20}
})
const profile = await authedClient('/user/profile')
const result = await authedClient('/user/update', {
method: 'POST',
body: {name: 'John Doe', email: 'john@example.com'}
})License
MIT