jsforce
Version:
Salesforce API Library for JavaScript
211 lines (210 loc) • 8.14 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setDefaults = void 0;
const stream_1 = require("stream");
const node_fetch_1 = __importStar(require("node-fetch"));
const https_proxy_agent_1 = __importDefault(require("https-proxy-agent"));
const request_helper_1 = require("./request-helper");
const logger_1 = require("./util/logger");
const is_1 = __importDefault(require("@sindresorhus/is"));
/**
*
*/
let defaults = {};
/**
*
*/
function setDefaults(defaults_) {
defaults = defaults_;
}
exports.setDefaults = setDefaults;
/**
*
*/
async function startFetchRequest(request, options, input, output, emitter, counter = 0) {
const logger = (0, logger_1.getLogger)('fetch');
const { httpProxy, followRedirect } = options;
const agent = httpProxy ? (0, https_proxy_agent_1.default)(httpProxy) : undefined;
const { url, body, ...rrequest } = request;
const controller = new AbortController();
let retryCount = 0;
const retryOpts = {
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, resOrErr) => {
if (!retryOpts.methods.includes(request.method))
return false;
if (resOrErr instanceof node_fetch_1.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_1.default.nodeStream(body) && stream_1.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) => {
const fetchOpts = {
...rrequest,
...(input && /^(post|put|patch)$/i.test(request.method)
? { body: input }
: {}),
redirect: 'manual',
signal: controller.signal,
agent,
};
try {
const res = await (0, node_fetch_1.default)(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;
// 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.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;
// Timeout after 60s 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 ?? 60000;
try {
res = await (0, request_helper_1.executeWithTimeout)(fetchWithRetries, fetchTimeout, () => controller.abort());
}
catch (err) {
emitter.emit('error', err);
return;
}
const headers = {};
for (const headerName of res.headers.keys()) {
headers[headerName.toLowerCase()] = res.headers.get(headerName);
}
const response = {
statusCode: res.status,
headers,
};
if (followRedirect && (0, request_helper_1.isRedirect)(response.statusCode)) {
try {
(0, request_helper_1.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);
}
/**
*
*/
function request(req, options_ = {}) {
const options = { ...defaults, ...options_ };
const { input, output, stream } = (0, request_helper_1.createHttpRequestHandlerStreams)(req, options);
startFetchRequest(req, options, input, output, stream);
return stream;
}
exports.default = request;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));