Sign In

@applitools/req

Package Overview
Dependencies
Maintainers
48
Versions
101
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@applitools/req - npm Package Compare versions

Comparing version
1.8.5
to
1.8.6
+856
README.md
# @applitools/req
A powerful, flexible HTTP request library with advanced features like retry logic, hooks, fallbacks, and timeout management.
## Table of Contents
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [API Reference](#api-reference)
- [req()](#req)
- [makeReq()](#makereq)
- [Options](#options)
- [Advanced Features](#advanced-features)
- [Retry Logic](#retry-logic)
- [Hooks](#hooks)
- [Fallbacks](#fallbacks)
- [Timeouts](#timeouts)
- [Chaining Options](#chaining-options)
- [Examples](#examples)
## Installation
```bash
yarn add @applitools/req
```
## Basic Usage
```typescript
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:**
```typescript
req(input: string | URL | Request, ...options: Options[]): Promise<Response>
```
**Parameters:**
- `input` - URL string, URL object, or Request object
- `options` - 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:**
```typescript
makeReq<TOptions>(baseOptions: Partial<TOptions>): Req<TOptions>
```
**Example:**
```typescript
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:**
```typescript
await req('./users', {baseUrl: 'https://api.example.com/v1'})
// Makes request to: https://api.example.com/v1/users
```
#### `method`
**Type:** `string`
HTTP method (uppercase). Overrides method from Request object.
**Example:**
```typescript
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:**
```typescript
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=active
```
#### `headers`
**Type:** `Record<string, string | string[] | undefined>`
HTTP headers. Merges with headers from Request object. Undefined values are filtered out.
**Example:**
```typescript
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:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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](#retry-logic) for detailed examples.
#### `hooks`
**Type:** `Hooks | Hooks[]`
Lifecycle hooks for request interception and modification.
See [Hooks](#hooks) for detailed examples.
#### `fallbacks`
**Type:** `Fallback | Fallback[]`
Fallback strategies for handling failures.
See [Fallbacks](#fallbacks) for detailed examples.
#### `keepAliveOptions`
**Type:** `{keepAlive: boolean; keepAliveMsecs?: number}`
HTTP agent keep-alive configuration.
**Example:**
```typescript
await req('https://api.example.com/data', {
keepAliveOptions: {
keepAlive: true,
keepAliveMsecs: 1000
}
})
```
#### `signal`
**Type:** `AbortSignal`
Abort signal for canceling requests.
**Example:**
```typescript
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
```typescript
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 with `connectionTimeout` or `retryTimeout`)
- **`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, only `statuses` and `codes` are checked
- **Retry trigger**: At least one of `statuses`, `codes`, or `validate` must match for a retry to occur
- **`Retry-After` header**: If present in the response, overrides the configured `timeout` value
#### Examples
**Retry on specific status codes:**
```typescript
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:**
```typescript
await req('https://api.example.com/data', {
retry: {
codes: ['ECONNRESET', 'ETIMEDOUT'],
limit: 5,
timeout: 2000
}
})
```
**Exponential backoff:**
```typescript
await req('https://api.example.com/data', {
retry: {
statuses: [429, 500],
limit: 4,
timeout: [1000, 2000, 4000, 8000] // Double delay each time
}
})
```
**Custom validation:**
```typescript
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:**
```typescript
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:**
```typescript
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 thrown
```
### Hooks
Hooks allow you to intercept and modify requests/responses at various lifecycle stages.
#### Available Hooks
```typescript
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:**
```typescript
await req('https://api.example.com/data', {
hooks: {
beforeRequest: ({request}) => {
request.headers.set('Authorization', `Bearer ${getToken()}`)
}
}
})
```
**Log all requests:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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
```typescript
interface Fallback {
shouldFallbackCondition: (options) => boolean | Promise<boolean>
updateOptions?: (options) => Options | Promise<Options>
cache?: Map<string, boolean>
}
```
#### Examples
**Fallback to different endpoint:**
```typescript
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:**
```typescript
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:**
```typescript
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.
```typescript
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 thrown
```
#### `requestTimeout`
Timeout for each individual request attempt. Throws `RequestTimeoutError` when exceeded.
```typescript
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.
```typescript
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 thrown
```
**Difference between timeouts:**
- `connectionTimeout`: Covers the entire request lifecycle including initial attempt and all retries
- `retryTimeout`: Only covers retry attempts (excludes the initial request)
- `requestTimeout`: Applies to each individual request attempt
#### Dynamic request timeout based on body size:
```typescript
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 seconds
```
## Chaining 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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
// 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
```typescript
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
+14
-0
# Changelog
## [1.8.6](https://github.com/Applitools-Dev/sdk/compare/js/req@1.8.5...js/req@1.8.6) (2025-11-19)
### Dependencies
* @applitools/utils bumped to 1.14.0
#### Features
* disable heartbeats whenever no tests are running ([#3344](https://github.com/Applitools-Dev/sdk/issues/3344)) ([b66d28a](https://github.com/Applitools-Dev/sdk/commit/b66d28a7a382f26b68de70c8633c027cb4bdf225))
* @applitools/logger bumped to 2.2.6
* @applitools/test-server bumped to 1.3.5
## [1.8.5](https://github.com/Applitools-Dev/sdk/compare/js/req@1.8.4...js/req@1.8.5) (2025-11-09)

@@ -4,0 +18,0 @@

+1
-0

@@ -5,1 +5,2 @@ export { stop } from './stop.js';

export { req as default } from './req.js';
export { RequestTimeoutError, ConnectionTimeoutError, RetryTimeoutError } from './req-errors.js';

@@ -5,2 +5,3 @@ export var AbortCode;

AbortCode["connectionTimeout"] = "MAX_TIMEOUT_REACHED";
AbortCode["retryTimeout"] = "RETRY_DURATION_EXCEEDED";
})(AbortCode || (AbortCode = {}));

@@ -21,1 +22,8 @@ export class RequestTimeoutError extends Error {

}
export class RetryTimeoutError extends Error {
constructor(timeout) {
super(`Retry duration of ${timeout} ms was exceeded.`);
this.code = AbortCode.retryTimeout;
this.name = 'RetryTimeoutError';
}
}
+161
-92
import { AbortController } from 'abort-controller';
import { stop } from './stop.js';
import { makeAgent } from './agent.js';
import { AbortCode, RequestTimeoutError, ConnectionTimeoutError } from './req-errors.js';
import { AbortCode, RequestTimeoutError, ConnectionTimeoutError, RetryTimeoutError } from './req-errors.js';
import globalFetch, { Request, Headers, Response } from './fetch.js';

@@ -19,2 +19,3 @@ import * as utils from '@applitools/utils';

let abortCode;
let retryStartTime = null;
if (options.baseUrl && !options.baseUrl.endsWith('/'))

@@ -50,14 +51,5 @@ options.baseUrl += '/';

}
async function singleReq(input, options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
const url = new URL(String((_a = input.url) !== null && _a !== void 0 ? _a : input), options.baseUrl);
const fetch = (_b = options.fetch) !== null && _b !== void 0 ? _b : globalFetch;
let optionsFallbacks = [];
if (options.fallbacks)
optionsFallbacks = utils.types.isArray(options.fallbacks) ? options.fallbacks : [options.fallbacks];
const fb = optionsFallbacks.find(fallback => { var _a; return (_a = fallback.cache) === null || _a === void 0 ? void 0 : _a.get(url.origin); });
if (fb === null || fb === void 0 ? void 0 : fb.updateOptions)
options = await fb.updateOptions({ options });
function setupRequestController(opts) {
const requestController = new AbortController();
const timeout = calculateTimeout(options.requestTimeout, options.body, options);
const timeout = calculateTimeout(opts.requestTimeout, opts.body, opts);
const requestTimer = timeout

@@ -69,12 +61,18 @@ ? setTimeout(() => {

: null;
const abortHandler = () => requestController.abort();
if (connectionController.signal.aborted)
requestController.abort();
connectionController.signal.onabort = () => requestController.abort();
if (options.signal) {
if (options.signal.aborted)
connectionController.signal.addEventListener('abort', abortHandler);
if (opts.signal) {
if (opts.signal.aborted)
requestController.abort();
options.signal.onabort = () => requestController.abort();
opts.signal.addEventListener('abort', abortHandler);
}
if (options.query) {
Object.entries(options.query).forEach(([key, value]) => {
return { requestController, requestTimer, abortHandler };
}
async function buildRequest(input, opts, requestController) {
var _a, _b, _c, _d, _e;
const url = new URL(String((_a = input.url) !== null && _a !== void 0 ? _a : input), opts.baseUrl);
if (opts.query) {
Object.entries(opts.query).forEach(([key, value]) => {
if (!utils.types.isNull(value))

@@ -85,76 +83,64 @@ url.searchParams.set(key, String(value));

const extraHeaders = {};
if (utils.types.isPlainObject(options.body) || utils.types.isArray(options.body) || options.body === null) {
options.body = JSON.stringify(options.body);
if (utils.types.isPlainObject(opts.body) || utils.types.isArray(opts.body) || opts.body === null) {
opts.body = JSON.stringify(opts.body);
extraHeaders['content-type'] = 'application/json';
}
let request = new Request(url, {
method: (_c = options.method) !== null && _c !== void 0 ? _c : input.method,
method: (_b = opts.method) !== null && _b !== void 0 ? _b : input.method,
headers: {
...extraHeaders,
...Object.fromEntries((_e = (_d = input.headers) === null || _d === void 0 ? void 0 : _d.entries()) !== null && _e !== void 0 ? _e : []),
...Object.fromEntries(new Headers(options.headers).entries()),
...Object.fromEntries((_d = (_c = input.headers) === null || _c === void 0 ? void 0 : _c.entries()) !== null && _d !== void 0 ? _d : []),
...Object.fromEntries(new Headers(opts.headers).entries()),
},
body: (_f = options.body) !== null && _f !== void 0 ? _f : input.body,
body: (_e = opts.body) !== null && _e !== void 0 ? _e : input.body,
highWaterMark: 1024 * 1024 * 100 + 1,
agent: makeAgent({
proxy: options.proxy,
useDnsCache: options.useDnsCache,
keepAliveOptions: options.keepAliveOptions,
proxy: opts.proxy,
useDnsCache: opts.useDnsCache,
keepAliveOptions: opts.keepAliveOptions,
}),
signal: requestController.signal,
});
request = await beforeRequest({ request, options });
try {
let response = await fetch(request);
// if the request has a fallback try it
if (!response.ok && optionsFallbacks.length > 0) {
const fallbackStrategy = optionsFallbacks[0];
const shouldFallback = await fallbackStrategy.shouldFallbackCondition({ request, response });
const fallbackOptions = shouldFallback &&
(await ((_g = fallbackStrategy === null || fallbackStrategy === void 0 ? void 0 : fallbackStrategy.updateOptions) === null || _g === void 0 ? void 0 : _g.call(fallbackStrategy, {
options: { ...options, fallbacks: optionsFallbacks.slice(1) },
})));
if (fallbackOptions) {
const fallbackStrategyResponse = await singleReq(request, fallbackOptions);
(_h = fallbackStrategy.cache) !== null && _h !== void 0 ? _h : (fallbackStrategy.cache = new Map());
fallbackStrategy.cache.set(new URL(request.url).origin, fallbackStrategyResponse.ok);
return fallbackStrategyResponse;
}
}
// if the request has to be retried due to status code
const retry = await ((_j = options.retry) === null || _j === void 0 ? void 0 : _j.reduce(async (prev, retry) => {
request = await beforeRequest({ request, options: opts });
return request;
}
async function tryFallback(request, response, opts, optionsFallbacks) {
var _a, _b;
if (response.ok || optionsFallbacks.length === 0)
return null;
const fallbackStrategy = optionsFallbacks[0];
const shouldFallback = await fallbackStrategy.shouldFallbackCondition({ request, response });
const fallbackOptions = shouldFallback &&
(await ((_a = fallbackStrategy === null || fallbackStrategy === void 0 ? void 0 : fallbackStrategy.updateOptions) === null || _a === void 0 ? void 0 : _a.call(fallbackStrategy, {
options: { ...opts, fallbacks: optionsFallbacks.slice(1) },
})));
if (fallbackOptions) {
const fallbackStrategyResponse = await singleReq(request, fallbackOptions);
(_b = fallbackStrategy.cache) !== null && _b !== void 0 ? _b : (fallbackStrategy.cache = new Map());
fallbackStrategy.cache.set(new URL(request.url).origin, fallbackStrategyResponse.ok);
return fallbackStrategyResponse;
}
return null;
}
async function findApplicableRetry(opts, context) {
const retries = opts.retry;
if (!retries)
return null;
if (context.response) {
return await retries.reduce(async (prev, retry) => {
var _a, _b;
const result = await prev;
return (result !== null && result !== void 0 ? result : ((((_a = retry.statuses) === null || _a === void 0 ? void 0 : _a.includes(response.status)) || (await ((_b = retry.validate) === null || _b === void 0 ? void 0 : _b.call(retry, { response })))) &&
return (result !== null && result !== void 0 ? result : ((((_a = retry.statuses) === null || _a === void 0 ? void 0 : _a.includes(context.response.status)) ||
(await ((_b = retry.validate) === null || _b === void 0 ? void 0 : _b.call(retry, { response: context.response })))) &&
(!retry.limit || !retry.attempt || retry.attempt < retry.limit)
? retry
: null));
}, Promise.resolve(null)));
if (retry) {
(_k = retry.attempt) !== null && _k !== void 0 ? _k : (retry.attempt = 0);
const delay = response.headers.has('Retry-After')
? Number(response.headers.get('Retry-After')) * 1000
: utils.types.isArray(retry.timeout)
? retry.timeout[Math.min(retry.attempt, retry.timeout.length - 1)]
: (_l = retry.timeout) !== null && _l !== void 0 ? _l : 0;
await utils.general.sleep(delay);
retry.attempt += 1;
const retryRequest = await beforeRetry({ request, response, attempt: retry.attempt, stop, options });
if (retryRequest !== stop) {
return singleReq(retryRequest, options);
}
}
response = await afterResponse({ request, response, options });
return response;
}, Promise.resolve(null));
}
catch (error) {
if (abortCode === AbortCode.requestTimeout)
error = new RequestTimeoutError();
else if (abortCode === AbortCode.connectionTimeout)
error = new ConnectionTimeoutError();
// if the request has to be retried due to network error
const retry = await ((_m = options.retry) === null || _m === void 0 ? void 0 : _m.reduce((prev, retry) => {
if (context.error) {
return await retries.reduce((prev, retry) => {
return prev.then(async (result) => {
var _a, _b;
return (result !== null && result !== void 0 ? result : ((((_a = retry.codes) === null || _a === void 0 ? void 0 : _a.includes(error.code)) || (await ((_b = retry.validate) === null || _b === void 0 ? void 0 : _b.call(retry, { error })))) &&
return (result !== null && result !== void 0 ? result : ((((_a = retry.codes) === null || _a === void 0 ? void 0 : _a.includes(context.error.code)) ||
(await ((_b = retry.validate) === null || _b === void 0 ? void 0 : _b.call(retry, { error: context.error })))) &&
(!retry.limit || !retry.attempt || retry.attempt < retry.limit)))

@@ -164,24 +150,107 @@ ? retry

});
}, Promise.resolve(null)));
if (retry) {
(_o = retry.attempt) !== null && _o !== void 0 ? _o : (retry.attempt = 0);
const delay = utils.types.isArray(retry.timeout)
? retry.timeout[Math.min(retry.attempt, retry.timeout.length)]
: (_p = retry.timeout) !== null && _p !== void 0 ? _p : 0;
await utils.general.sleep(delay);
retry.attempt = retry.attempt + 1;
const retryRequest = await beforeRetry({ request, error, attempt: retry.attempt, stop, options });
if (retryRequest !== stop) {
return singleReq(retryRequest, options);
}, Promise.resolve(null));
}
return null;
}
function calculateRetryDelay(retry, response) {
var _a, _b;
if (response === null || response === void 0 ? void 0 : response.headers.has('Retry-After')) {
return Number(response.headers.get('Retry-After')) * 1000;
}
if (utils.types.isArray(retry.timeout)) {
return retry.timeout[Math.min((_a = retry.attempt) !== null && _a !== void 0 ? _a : 0, retry.timeout.length - 1)];
}
return (_b = retry.timeout) !== null && _b !== void 0 ? _b : 0;
}
function checkRetryTimeout() {
if (options.retryTimeout && retryStartTime && Date.now() - retryStartTime >= options.retryTimeout) {
throw new RetryTimeoutError(options.retryTimeout);
}
}
async function handleRetry(request, retry, context, opts) {
var _a;
(_a = retry.attempt) !== null && _a !== void 0 ? _a : (retry.attempt = 0);
retryStartTime !== null && retryStartTime !== void 0 ? retryStartTime : (retryStartTime = Date.now());
checkRetryTimeout();
const delay = calculateRetryDelay(retry, context.response);
await utils.general.sleep(delay);
retry.attempt += 1;
return await beforeRetry({
request,
...context,
attempt: retry.attempt,
stop,
options: opts,
});
}
function normalizeAbortError(error) {
if (abortCode === AbortCode.requestTimeout)
return new RequestTimeoutError();
if (abortCode === AbortCode.connectionTimeout)
return new ConnectionTimeoutError();
return error;
}
function cleanupRequest(opts, requestTimer, abortHandler) {
if (requestTimer)
clearTimeout(requestTimer);
// Remove the abort listeners we attached
connectionController.signal.removeEventListener('abort', abortHandler);
if (opts.signal) {
opts.signal.removeEventListener('abort', abortHandler);
}
}
async function singleReq(input, options) {
var _a, _b;
const fetch = (_a = options.fetch) !== null && _a !== void 0 ? _a : globalFetch;
let optionsFallbacks = [];
if (options.fallbacks)
optionsFallbacks = utils.types.isArray(options.fallbacks) ? options.fallbacks : [options.fallbacks];
// Apply cached fallback options if available
const url = new URL(String((_b = input.url) !== null && _b !== void 0 ? _b : input), options.baseUrl);
const fb = optionsFallbacks.find(fallback => { var _a; return (_a = fallback.cache) === null || _a === void 0 ? void 0 : _a.get(url.origin); });
if (fb === null || fb === void 0 ? void 0 : fb.updateOptions)
options = await fb.updateOptions({ options });
while (true) {
const { requestController, requestTimer, abortHandler } = setupRequestController(options);
const request = await buildRequest(input, options, requestController);
try {
let response = await fetch(request);
// Try fallback if needed
const fallbackResponse = await tryFallback(request, response, options, optionsFallbacks);
if (fallbackResponse)
return fallbackResponse;
// Check if retry is needed for status code
const retry = await findApplicableRetry(options, { response });
if (retry) {
const retryRequest = await handleRetry(request, retry, { response }, options);
if (retryRequest !== stop) {
cleanupRequest(options, requestTimer, abortHandler);
input = retryRequest;
continue;
}
}
// Success - return response
response = await afterResponse({ request, response, options });
return response;
}
error = await afterError({ request, error, options });
throw error;
catch (error) {
error = normalizeAbortError(error);
// Check if retry is needed for network error
const retry = await findApplicableRetry(options, { error });
if (retry) {
const retryRequest = await handleRetry(request, retry, { error }, options);
if (retryRequest !== stop) {
cleanupRequest(options, requestTimer, abortHandler);
input = retryRequest;
continue;
}
}
// No retry - throw error
error = await afterError({ request, error, options });
throw error;
}
finally {
cleanupRequest(options, requestTimer, abortHandler);
}
}
finally {
if (options.signal)
options.signal.onabort = null;
if (requestTimer)
clearTimeout(requestTimer);
}
}

@@ -188,0 +257,0 @@ }

{
"name": "@applitools/req",
"version": "1.8.5",
"version": "1.8.6",
"description": "Applitools fetch-based request library",

@@ -60,3 +60,3 @@ "keywords": [

"dependencies": {
"@applitools/utils": "1.13.0",
"@applitools/utils": "1.14.0",
"abort-controller": "3.0.0",

@@ -69,3 +69,3 @@ "http-proxy-agent": "5.0.0",

"@applitools/api-extractor": "^1.2.22",
"@applitools/test-server": "^1.3.4",
"@applitools/test-server": "^1.3.5",
"@applitools/test-utils": "^1.5.17",

@@ -72,0 +72,0 @@ "@types/node": "^12.20.55",

@@ -87,2 +87,14 @@ export type Stop = stop;

export default req;
export class RequestTimeoutError extends Error {
constructor();
code: AbortCode.requestTimeout | AbortCode.connectionTimeout | AbortCode.retryTimeout;
}
export class ConnectionTimeoutError extends Error {
constructor();
code: AbortCode.requestTimeout | AbortCode.connectionTimeout | AbortCode.retryTimeout;
}
export class RetryTimeoutError extends Error {
constructor(timeout: number);
code: AbortCode.requestTimeout | AbortCode.connectionTimeout | AbortCode.retryTimeout;
}
export type Fetch = (url: URL | (string | Request), init?: undefined | RequestInit) => Promise<Response>;

@@ -99,2 +111,3 @@ export interface Options {

requestTimeout?: undefined | number | { base: number; perByte: number; };
retryTimeout?: undefined | number;
retry?: undefined | Retry | Array<Retry>;

@@ -101,0 +114,0 @@ hooks?: undefined | Hooks<this> | Array<Hooks<this>>;

@@ -87,2 +87,14 @@ export type Stop = stop;

export default req;
export class RequestTimeoutError extends Error {
constructor();
code: AbortCode.requestTimeout | AbortCode.connectionTimeout | AbortCode.retryTimeout;
}
export class ConnectionTimeoutError extends Error {
constructor();
code: AbortCode.requestTimeout | AbortCode.connectionTimeout | AbortCode.retryTimeout;
}
export class RetryTimeoutError extends Error {
constructor(timeout: number);
code: AbortCode.requestTimeout | AbortCode.connectionTimeout | AbortCode.retryTimeout;
}
export type Fetch = (url: URL | (string | Request), init?: undefined | RequestInit) => Promise<Response>;

@@ -99,2 +111,3 @@ export interface Options {

requestTimeout?: undefined | number | { base: number; perByte: number; };
retryTimeout?: undefined | number;
retry?: undefined | Retry | Array<Retry>;

@@ -101,0 +114,0 @@ hooks?: undefined | Hooks<this> | Array<Hooks<this>>;

Sorry, the diff of this file is too big to display