@applitools/req
Advanced tools
| import { fetch as undiciFetch, Agent } from 'undici'; | ||
| import { Response as NodeFetchResponse } from 'node-fetch'; | ||
| import { lookupWithCache } from './dns-cache.js'; | ||
| import { Readable } from 'stream'; | ||
| import { Buffer } from 'buffer'; | ||
| import * as utils from '@applitools/utils'; | ||
| const cachify = utils.general.cachify; | ||
| const cachifiedCreateAgent = cachify((options) => { | ||
| return new Agent({ | ||
| allowH2: options.allowH2, | ||
| maxHeaderSize: 16384, | ||
| keepAliveTimeout: options.keepAliveTimeout, | ||
| keepAliveMaxTimeout: options.keepAliveMaxTimeout, | ||
| connect: { timeout: 60000, lookup: options.useDnsCache ? lookupWithCache : undefined }, | ||
| bodyTimeout: 300000, | ||
| headersTimeout: 300000, | ||
| }); | ||
| }); | ||
| const makeDispatcher = cachify(() => { | ||
| return function dispatcher(url) { | ||
| const isHttps = url.protocol === 'https:'; | ||
| // HTTP/2 requires TLS — only allow H2 for HTTPS URLs | ||
| const allowH2 = isHttps; | ||
| const useDnsCache = utils.general.getEnvValue('USE_DNS_CACHE') === 'true'; | ||
| const agentOptions = { | ||
| allowH2, | ||
| keepAliveTimeout: 30000, | ||
| keepAliveMaxTimeout: 300000, | ||
| useDnsCache, | ||
| }; | ||
| return cachifiedCreateAgent(agentOptions); | ||
| }; | ||
| }); | ||
| // --- OPTIMIZATION 1: Get Body Length without reading the body --- | ||
| function getBodyLength(url, body, headers) { | ||
| if (url.size) { | ||
| return String(url.size); | ||
| } | ||
| if (headers && (headers['content-length'] || headers['Content-Length'])) { | ||
| return headers['content-length'] || headers['Content-Length']; | ||
| } | ||
| if (Buffer.isBuffer(body)) | ||
| return String(body.length); | ||
| if (typeof body === 'string') | ||
| return String(Buffer.byteLength(body)); | ||
| if (body && body.size) | ||
| return String(body.size); | ||
| return undefined; | ||
| } | ||
| export const fetchImpl = async (url, options = {}) => { | ||
| var _a, _b, _c, _d, _e; | ||
| let finalUrl; | ||
| const finalOptions = { ...options }; | ||
| // 1. Parameter Normalization | ||
| if (typeof url === 'object' && 'url' in url && !(url instanceof URL)) { | ||
| const req = url; // Treated as NodeFetchRequest | ||
| finalUrl = req.url; | ||
| (_a = finalOptions.method) !== null && _a !== void 0 ? _a : (finalOptions.method = req.method); | ||
| (_b = finalOptions.headers) !== null && _b !== void 0 ? _b : (finalOptions.headers = {}); | ||
| // Copy headers from the request object | ||
| if (typeof ((_c = req.headers) === null || _c === void 0 ? void 0 : _c.forEach) === 'function') { | ||
| req.headers.forEach((v, k) => { | ||
| finalOptions.headers[k.toLowerCase()] = v; | ||
| }); | ||
| } | ||
| else { | ||
| Object.assign(finalOptions.headers, req.headers); | ||
| } | ||
| // --- OPTIMIZATION 2: Don't Buffer. Use the stream directly. --- | ||
| // If we haven't provided a body, use the one from the request. | ||
| if (!finalOptions.body) { | ||
| finalOptions.body = req.body; | ||
| } | ||
| (_d = finalOptions.timeout) !== null && _d !== void 0 ? _d : (finalOptions.timeout = req.timeout); | ||
| } | ||
| else { | ||
| finalUrl = url; | ||
| } | ||
| // 2. Dispatcher Setup | ||
| if (!finalOptions.dispatcher) { | ||
| const urlObj = typeof finalUrl === 'string' ? new URL(finalUrl) : finalUrl; | ||
| finalOptions.dispatcher = (_e = makeDispatcher()) === null || _e === void 0 ? void 0 : _e(urlObj); | ||
| } | ||
| // 3. Stream Handling (Crucial for Images) | ||
| const isStream = finalOptions.body && | ||
| (typeof finalOptions.body.pipe === 'function' || typeof finalOptions.body[Symbol.asyncIterator] === 'function'); | ||
| if (isStream) { | ||
| // Undici MUST have 'duplex: half' to stream a request body | ||
| finalOptions.duplex = 'half'; | ||
| } | ||
| const detectedSize = getBodyLength(url, finalOptions.body, finalOptions.headers); | ||
| if (detectedSize) { | ||
| // set content-length header only if it's not already set by the user, and if we were able to detect the size | ||
| const contentLengthHeader = Object.keys(finalOptions.headers).find((h) => h.toLowerCase() === 'content-length'); | ||
| if (!contentLengthHeader) { | ||
| finalOptions.headers = { ...finalOptions.headers, 'content-length': detectedSize }; | ||
| } | ||
| } | ||
| let timeoutId; | ||
| let internalController; | ||
| if (finalOptions.timeout) { | ||
| internalController = new AbortController(); | ||
| timeoutId = setTimeout(() => internalController === null || internalController === void 0 ? void 0 : internalController.abort(), finalOptions.timeout); | ||
| if (finalOptions.signal) { | ||
| finalOptions.signal.addEventListener('abort', () => internalController === null || internalController === void 0 ? void 0 : internalController.abort(), { once: true }); | ||
| } | ||
| finalOptions.signal = internalController.signal; | ||
| } | ||
| // Cleanup | ||
| delete finalOptions.timeout; | ||
| delete finalOptions.agent; | ||
| delete finalOptions.size; | ||
| try { | ||
| const response = await undiciFetch(finalUrl, finalOptions); | ||
| // Return stream to caller (Don't buffer response either!) | ||
| const nodeBody = response.body ? Readable.from(response.body) : undefined; | ||
| return new NodeFetchResponse(nodeBody, { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| headers: response.headers, | ||
| }); | ||
| } | ||
| finally { | ||
| if (timeoutId) | ||
| clearTimeout(timeoutId); | ||
| } | ||
| }; | ||
| // utility function to make transition from node-fetch to undici easier | ||
| // and to avoid converting streams to buffers just to get their length in fetch-http2 implementation | ||
| export function getBodySize(input, opts) { | ||
| var _a, _b; | ||
| // 1. Determine Method EARLY so we can decide if body is allowed | ||
| const method = (_a = opts.method) !== null && _a !== void 0 ? _a : input.method; | ||
| const isBodyAllowed = method && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD'; | ||
| let finalSize = undefined; | ||
| let finalBody = (_b = opts.body) !== null && _b !== void 0 ? _b : input.body; | ||
| if (isBodyAllowed) { | ||
| if (opts.size) { | ||
| finalSize = opts.size; | ||
| } | ||
| else { | ||
| if (Buffer.isBuffer(finalBody)) { | ||
| finalSize = finalBody.length; | ||
| } | ||
| else if (finalBody instanceof Uint8Array) { | ||
| finalSize = finalBody.byteLength; | ||
| } | ||
| else if (typeof finalBody === 'string') { | ||
| finalSize = Buffer.byteLength(finalBody); | ||
| } | ||
| else if (utils.types.isPlainObject(finalBody) || utils.types.isArray(finalBody) || finalBody === null) { | ||
| finalBody = JSON.stringify(finalBody); | ||
| finalSize = Buffer.byteLength(finalBody); | ||
| } | ||
| } | ||
| } | ||
| return finalSize; | ||
| } |
+7
-0
| # Changelog | ||
| ## [1.9.0](https://github.com/Applitools-Dev/sdk/compare/js/req@1.8.8...js/req@1.9.0) (2026-03-02) | ||
| ### Features | ||
| * http 2 support | FLD-4085 ([#3539](https://github.com/Applitools-Dev/sdk/issues/3539)) ([b304d4a](https://github.com/Applitools-Dev/sdk/commit/b304d4a1072a201d1c3f5cea1275b6a15230d6aa)) | ||
| ## [1.8.8](https://github.com/Applitools-Dev/sdk/compare/js/req@1.8.7...js/req@1.8.8) (2026-02-16) | ||
@@ -4,0 +11,0 @@ |
@@ -5,1 +5,2 @@ export default globalThis.fetch; | ||
| export const Response = globalThis.Response; | ||
| export const getBodySize = () => undefined; |
+30
-2
@@ -1,2 +0,30 @@ | ||
| export { default } from 'node-fetch'; | ||
| export * from 'node-fetch'; | ||
| import { general } from '@applitools/utils'; | ||
| const cachify = general.cachify; | ||
| const getFetchImpl = cachify(async (httpVersion) => { | ||
| let fetchImpl; | ||
| let getBodySize = () => undefined; | ||
| if (httpVersion === '2') { | ||
| const module = await import('./fetch-http2.js'); | ||
| fetchImpl = module.fetchImpl; | ||
| getBodySize = module.getBodySize; | ||
| } | ||
| else { | ||
| const module = await import('node-fetch'); | ||
| fetchImpl = module.default; | ||
| getBodySize = () => undefined; | ||
| } | ||
| return { fetchImpl, getBodySize }; | ||
| }); | ||
| // Export a wrapper that selects implementation based on environment variable | ||
| export default async function fetch(...args) { | ||
| const httpVersion = general.getEnvValue('HTTP_VERSION') || ''; | ||
| const { fetchImpl } = await getFetchImpl(httpVersion); | ||
| return fetchImpl(...args); | ||
| } | ||
| export const getBodySize = async (...args) => { | ||
| const httpVersion = general.getEnvValue('HTTP_VERSION') || ''; | ||
| const { getBodySize } = await getFetchImpl(httpVersion); | ||
| return getBodySize(...args); | ||
| }; | ||
| // Export runtime classes and types from node-fetch (both implementations use these same types) | ||
| export { Request, Headers, Response } from 'node-fetch'; |
+8
-4
@@ -1,6 +0,5 @@ | ||
| import { AbortController } from 'abort-controller'; | ||
| import { stop } from './stop.js'; | ||
| import { makeAgent } from './agent.js'; | ||
| import { AbortCode, RequestTimeoutError, ConnectionTimeoutError, RetryTimeoutError } from './req-errors.js'; | ||
| import globalFetch, { Request, Headers, Response } from './fetch.js'; | ||
| import globalFetch, { Request, Headers, Response, getBodySize } from './fetch.js'; | ||
| import * as utils from '@applitools/utils'; | ||
@@ -84,3 +83,3 @@ import { Buffer } from 'buffer'; | ||
| } | ||
| let request = new Request(url, { | ||
| const requestInit = { | ||
| method: (_b = opts.method) !== null && _b !== void 0 ? _b : input.method, | ||
@@ -100,3 +99,8 @@ headers: { | ||
| signal: requestController.signal, | ||
| }); | ||
| }; | ||
| const finalSize = await getBodySize(input, opts); | ||
| if (finalSize) { | ||
| requestInit.size = finalSize; | ||
| } | ||
| let request = new Request(url, requestInit); | ||
| request = await beforeRequest({ request, options: opts }); | ||
@@ -103,0 +107,0 @@ return request; |
+8
-3
| { | ||
| "name": "@applitools/req", | ||
| "version": "1.8.8", | ||
| "version": "1.9.0", | ||
| "description": "Applitools fetch-based request library", | ||
@@ -46,2 +46,3 @@ "keywords": [ | ||
| "./dist/agent.js": "./dist/agent-browser.js", | ||
| "./dist/fetch-http2.js": "./dist/fetch-browser.js", | ||
| "./dist/fetch.js": "./dist/fetch-browser.js" | ||
@@ -58,7 +59,8 @@ }, | ||
| "build:cjs": "esbuild ./dist/index.js --outfile=./dist/index.cjs --bundle --platform=node --format=cjs --target=node12 && cp ./types/index.d.ts ./types/index.d.cts", | ||
| "test": "run --top-level mocha './test/**/*.spec.ts'" | ||
| "test": "MOCHA_OMIT_TAGS=http2 run --top-level mocha './test/**/*.spec.ts'", | ||
| "test:http2": "APPLITOOLS_HTTP_VERSION=2 MOCHA_OMIT_TAGS=http1 run --top-level mocha './test/**/*.spec.ts'" | ||
| }, | ||
| "dependencies": { | ||
| "@applitools/logger": "2.2.8", | ||
| "@applitools/utils": "1.14.1", | ||
| "abort-controller": "3.0.0", | ||
| "http-proxy-agent": "5.0.0", | ||
@@ -80,2 +82,5 @@ "https-proxy-agent": "5.0.1", | ||
| }, | ||
| "optionalDependencies": { | ||
| "undici": ">=6.23.0 <8.0.0" | ||
| }, | ||
| "publishConfig": { | ||
@@ -82,0 +87,0 @@ "access": "public" |
+26
-25
@@ -23,21 +23,2 @@ export type Stop = stop; | ||
| } | ||
| export interface RequestInit { | ||
| body?: undefined | null | BodyInit; | ||
| headers?: undefined | HeadersInit; | ||
| method?: undefined | string; | ||
| redirect?: undefined | ("error" | "follow" | "manual"); | ||
| signal?: undefined | null | { readonly aborted: boolean; addEventListener: (type: "abort", listener: () => void) => void; removeEventListener: (type: "abort", listener: () => void) => void; }; | ||
| referrer?: undefined | string; | ||
| referrerPolicy?: undefined | ("" | "no-referrer" | "no-referrer-when-downgrade" | "same-origin" | "origin" | "strict-origin" | "origin-when-cross-origin" | "strict-origin-when-cross-origin" | "unsafe-url"); | ||
| agent?: undefined | boolean | import('http').Agent | ((parsedUrl: URL) => undefined | boolean | import('http').Agent); | ||
| compress?: undefined | boolean; | ||
| counter?: undefined | number; | ||
| follow?: undefined | number; | ||
| hostname?: undefined | string; | ||
| port?: undefined | number; | ||
| protocol?: undefined | string; | ||
| size?: undefined | number; | ||
| highWaterMark?: undefined | number; | ||
| insecureHTTPParser?: undefined | boolean; | ||
| } | ||
| export class Response { | ||
@@ -66,7 +47,2 @@ static error(): Response; | ||
| } | ||
| export interface ResponseInit { | ||
| headers?: undefined | HeadersInit; | ||
| status?: undefined | number; | ||
| statusText?: undefined | string; | ||
| } | ||
| export class Headers { | ||
@@ -86,2 +62,26 @@ constructor(init?: undefined | HeadersInit); | ||
| } | ||
| export interface RequestInit { | ||
| body?: undefined | null | BodyInit; | ||
| headers?: undefined | HeadersInit; | ||
| method?: undefined | string; | ||
| redirect?: undefined | ("error" | "follow" | "manual"); | ||
| signal?: undefined | null | { readonly aborted: boolean; addEventListener: (type: "abort", listener: () => void) => void; removeEventListener: (type: "abort", listener: () => void) => void; }; | ||
| referrer?: undefined | string; | ||
| referrerPolicy?: undefined | ("" | "no-referrer" | "no-referrer-when-downgrade" | "same-origin" | "origin" | "strict-origin" | "origin-when-cross-origin" | "strict-origin-when-cross-origin" | "unsafe-url"); | ||
| agent?: undefined | boolean | import('http').Agent | ((parsedUrl: URL) => undefined | boolean | import('http').Agent); | ||
| compress?: undefined | boolean; | ||
| counter?: undefined | number; | ||
| follow?: undefined | number; | ||
| hostname?: undefined | string; | ||
| port?: undefined | number; | ||
| protocol?: undefined | string; | ||
| size?: undefined | number; | ||
| highWaterMark?: undefined | number; | ||
| insecureHTTPParser?: undefined | boolean; | ||
| } | ||
| export interface ResponseInit { | ||
| headers?: undefined | HeadersInit; | ||
| status?: undefined | number; | ||
| statusText?: undefined | string; | ||
| } | ||
| export type HeadersInit = Record<string, string> | Headers | Iterable<[string, string]> | Iterable<Iterable<string>>; | ||
@@ -102,3 +102,3 @@ export type BodyInit = string | NodeJS.ReadableStream | Blob | Buffer | URLSearchParams | FormData; | ||
| } | ||
| export type Fetch = (url: URL | (string | Request), init?: undefined | RequestInit) => Promise<Response>; | ||
| export type Fetch = (...args: Array<any>) => Promise<any>; | ||
| export interface Options { | ||
@@ -121,2 +121,3 @@ baseUrl?: undefined | string; | ||
| keepAliveOptions?: undefined | KeepAliveOptions; | ||
| size?: undefined | number; | ||
| } | ||
@@ -123,0 +124,0 @@ export type Retry = { |
+26
-25
@@ -23,21 +23,2 @@ export type Stop = stop; | ||
| } | ||
| export interface RequestInit { | ||
| body?: undefined | null | BodyInit; | ||
| headers?: undefined | HeadersInit; | ||
| method?: undefined | string; | ||
| redirect?: undefined | ("error" | "follow" | "manual"); | ||
| signal?: undefined | null | { readonly aborted: boolean; addEventListener: (type: "abort", listener: () => void) => void; removeEventListener: (type: "abort", listener: () => void) => void; }; | ||
| referrer?: undefined | string; | ||
| referrerPolicy?: undefined | ("" | "no-referrer" | "no-referrer-when-downgrade" | "same-origin" | "origin" | "strict-origin" | "origin-when-cross-origin" | "strict-origin-when-cross-origin" | "unsafe-url"); | ||
| agent?: undefined | boolean | import('http').Agent | ((parsedUrl: URL) => undefined | boolean | import('http').Agent); | ||
| compress?: undefined | boolean; | ||
| counter?: undefined | number; | ||
| follow?: undefined | number; | ||
| hostname?: undefined | string; | ||
| port?: undefined | number; | ||
| protocol?: undefined | string; | ||
| size?: undefined | number; | ||
| highWaterMark?: undefined | number; | ||
| insecureHTTPParser?: undefined | boolean; | ||
| } | ||
| export class Response { | ||
@@ -66,7 +47,2 @@ static error(): Response; | ||
| } | ||
| export interface ResponseInit { | ||
| headers?: undefined | HeadersInit; | ||
| status?: undefined | number; | ||
| statusText?: undefined | string; | ||
| } | ||
| export class Headers { | ||
@@ -86,2 +62,26 @@ constructor(init?: undefined | HeadersInit); | ||
| } | ||
| export interface RequestInit { | ||
| body?: undefined | null | BodyInit; | ||
| headers?: undefined | HeadersInit; | ||
| method?: undefined | string; | ||
| redirect?: undefined | ("error" | "follow" | "manual"); | ||
| signal?: undefined | null | { readonly aborted: boolean; addEventListener: (type: "abort", listener: () => void) => void; removeEventListener: (type: "abort", listener: () => void) => void; }; | ||
| referrer?: undefined | string; | ||
| referrerPolicy?: undefined | ("" | "no-referrer" | "no-referrer-when-downgrade" | "same-origin" | "origin" | "strict-origin" | "origin-when-cross-origin" | "strict-origin-when-cross-origin" | "unsafe-url"); | ||
| agent?: undefined | boolean | import('http').Agent | ((parsedUrl: URL) => undefined | boolean | import('http').Agent); | ||
| compress?: undefined | boolean; | ||
| counter?: undefined | number; | ||
| follow?: undefined | number; | ||
| hostname?: undefined | string; | ||
| port?: undefined | number; | ||
| protocol?: undefined | string; | ||
| size?: undefined | number; | ||
| highWaterMark?: undefined | number; | ||
| insecureHTTPParser?: undefined | boolean; | ||
| } | ||
| export interface ResponseInit { | ||
| headers?: undefined | HeadersInit; | ||
| status?: undefined | number; | ||
| statusText?: undefined | string; | ||
| } | ||
| export type HeadersInit = Record<string, string> | Headers | Iterable<[string, string]> | Iterable<Iterable<string>>; | ||
@@ -102,3 +102,3 @@ export type BodyInit = string | NodeJS.ReadableStream | Blob | Buffer | URLSearchParams | FormData; | ||
| } | ||
| export type Fetch = (url: URL | (string | Request), init?: undefined | RequestInit) => Promise<Response>; | ||
| export type Fetch = (...args: Array<any>) => Promise<any>; | ||
| export interface Options { | ||
@@ -121,2 +121,3 @@ baseUrl?: undefined | string; | ||
| keepAliveOptions?: undefined | KeepAliveOptions; | ||
| size?: undefined | number; | ||
| } | ||
@@ -123,0 +124,0 @@ export type Retry = { |
Sorry, the diff of this file is too big to display
Potential vulnerability
Supply chain riskInitial human review suggests the presence of a vulnerability in this package. It is pending further analysis and confirmation.
Network access
Supply chain riskThis module accesses the network.
Found 5 instances
Potential vulnerability
Supply chain riskInitial human review suggests the presence of a vulnerability in this package. It is pending further analysis and confirmation.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1300878
153.56%18
5.88%30236
162.81%2
-93.1%12
-53.85%6
20%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed