UNPKG

jsforce

Version:

Salesforce API Library for JavaScript

211 lines (210 loc) 8.14 kB
"use strict"; 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));