PDF_Generation_and_Automation/node_modules/jsforce/src/request.ts
2025-08-24 12:01:08 +05:30

246 lines
6.5 KiB
TypeScript

import { EventEmitter } from 'events';
import { Duplex, Readable, Writable } from 'stream';
import fetch, { Response, RequestInit, FetchError } from 'node-fetch';
import createHttpsProxyAgent from 'https-proxy-agent';
import {
createHttpRequestHandlerStreams,
executeWithTimeout,
isRedirect,
performRedirectRequest,
} from './request-helper';
import { HttpRequest, HttpRequestOptions } from './types';
import { getLogger } from './util/logger';
import is from '@sindresorhus/is';
/**
*
*/
let defaults: HttpRequestOptions = {};
/**
*
*/
export function setDefaults(defaults_: HttpRequestOptions) {
defaults = defaults_;
}
/**
*
*/
async function startFetchRequest(
request: HttpRequest,
options: HttpRequestOptions,
input: Readable | undefined,
output: Writable,
emitter: EventEmitter,
counter: number = 0,
) {
const logger = getLogger('fetch');
const { httpProxy, followRedirect } = options;
const agent = httpProxy ? createHttpsProxyAgent(httpProxy) : undefined;
const { url, body, ...rrequest } = request;
const controller = new AbortController();
let retryCount = 0;
const retryOpts: Required<HttpRequestOptions['retry']> = {
statusCodes: options.retry?.statusCodes ?? [420, 429, 500, 502, 503, 504],
maxRetries: options.retry?.maxRetries ?? 5,
minTimeout: options.retry?.minTimeout ?? 500,
timeoutFactor: options.retry?.timeoutFactor ?? 2,
errorCodes: options.retry?.errorCodes ?? [
'ECONNRESET',
'ECONNREFUSED',
'ENOTFOUND',
'ENETDOWN',
'ENETUNREACH',
'EHOSTDOWN',
'UND_ERR_SOCKET',
'ETIMEDOUT',
'EPIPE',
],
methods: options.retry?.methods ?? [
'GET',
'PUT',
'HEAD',
'OPTIONS',
'DELETE',
],
};
const shouldRetryRequest = (
maxRetry: number,
resOrErr: Response | Error | FetchError,
): boolean => {
if (!retryOpts.methods.includes(request.method)) return false;
if (resOrErr instanceof Response) {
if (retryOpts.statusCodes.includes(resOrErr.status)) {
if (maxRetry === retryCount) {
return false
} else {
return true;
}
}
return false;
} else {
if (maxRetry === retryCount) return false;
// only retry on operational errors
// https://github.com/node-fetch/node-fetch/blob/2.x/ERROR-HANDLING.md#error-handling-with-node-fetch
if (resOrErr.name != 'FetchError') return false;
if (is.nodeStream(body) && Readable.isDisturbed(body)) {
logger.debug('Body of type stream was read, unable to retry request.');
return false;
}
if (
'code' in resOrErr &&
resOrErr.code &&
retryOpts?.errorCodes?.includes(resOrErr.code)
)
return true;
return false;
}
};
const fetchWithRetries = async (
maxRetry = retryOpts?.maxRetries,
): Promise<Response> => {
const fetchOpts: RequestInit = {
...rrequest,
...(input && /^(post|put|patch)$/i.test(request.method)
? { body: input }
: {}),
redirect: 'manual',
signal: controller.signal,
agent,
};
try {
const res = await fetch(url, fetchOpts);
if (shouldRetryRequest(retryOpts.maxRetries, res)) {
logger.debug(`retrying for the ${retryCount + 1} time`);
logger.debug('reason: statusCode match');
await sleep(
retryCount === 0
? retryOpts.minTimeout
: retryOpts.minTimeout * retryOpts.timeoutFactor ** retryCount,
);
// NOTE: this event is only used by tests and will be removed at any time.
// jsforce may switch to node's fetch which doesn't emit this event on retries.
emitter.emit('retry', retryCount);
retryCount++;
return await fetchWithRetries(maxRetry);
}
// should we throw here if the maxRetry already happened and still got the same statusCode?
return res;
} catch (err) {
logger.debug('Request failed');
const error = err as Error | FetchError;
// request was canceled by consumer (AbortController), skip retry and rethrow.
if (error.name === 'AbortError') {
throw error;
}
if (shouldRetryRequest(retryOpts.maxRetries, error)) {
logger.debug(`retrying for the ${retryCount + 1} time`);
logger.debug(`Error: ${(err as Error).message}`);
await sleep(
retryCount === 0
? retryOpts.minTimeout
: retryOpts.minTimeout * retryOpts.timeoutFactor ** retryCount,
);
// NOTE: this event is only used by tests and will be removed at any time.
// jsforce may switch to node's fetch which doesn't emit this event on retries.
emitter.emit('retry', retryCount);
retryCount++;
return fetchWithRetries(maxRetry);
}
logger.debug('Skipping retry...');
if (maxRetry === retryCount) {
throw err;
} else {
throw err;
}
}
};
let res: Response;
// Timeout after 5 minutes without a response
//
// node-fetch's default timeout is 0 and jsforce consumers can't set this when calling `Connection` methods so we set a long default at the fetch wrapper level.
const fetchTimeout = options.timeout ?? 300_000
try {
res = await executeWithTimeout(fetchWithRetries, fetchTimeout, () =>
controller.abort(),
);
} catch (err) {
emitter.emit('error', err);
return;
}
const headers: { [key: string]: any } = {};
for (const headerName of res.headers.keys()) {
headers[headerName.toLowerCase()] = res.headers.get(headerName);
}
const response = {
statusCode: res.status,
headers,
};
if (followRedirect && isRedirect(response.statusCode)) {
try {
performRedirectRequest(
request,
response,
followRedirect,
counter,
(req) =>
startFetchRequest(
req,
options,
undefined,
output,
emitter,
counter + 1,
),
);
} catch (err) {
emitter.emit('error', err);
}
return;
}
emitter.emit('response', response);
res.body.pipe(output);
}
/**
*
*/
export default function request(
req: HttpRequest,
options_: HttpRequestOptions = {},
): Duplex {
const options = { ...defaults, ...options_ };
const { input, output, stream } = createHttpRequestHandlerStreams(
req,
options,
);
startFetchRequest(req, options, input, output, stream);
return stream;
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));