UNPKG

@sap/xssec

Version:

XS Advanced Container Security API for node.js

164 lines (138 loc) 5.42 kB
const fetch = require("./fetch"); const { getLogger } = require("./logging"); const RequestError = require("../error/network/RequestError"); const RetryError = require("../error/network/RetryError"); const ResponseError = require("../error/network/ResponseError"); const TimeoutError = require("../error/network/TimeoutError"); const {CORRELATIONID_HEADER_VCAP, DEFAULT_TIMEOUT, MAX_TIMEOUT, USER_AGENT } = require('./constants'); const { shrinkRequestOptionsForLog } = require("./util"); const LOG = getLogger("jsonRequest.js"); /** * Sleep for the given number of milliseconds * @param {number} ms - milliseconds to sleep * @returns {Promise<void>} */ const sleep = (ms) => new Promise(resolve => { setTimeout(()=> { resolve(); }, ms) }); /** * Calculate delay for retry based on attempt number and retry configuration * @param {number} attempt - the current attempt number (0-based) * @param {Object} retryConfig - retry configuration * @returns {number} - delay in milliseconds */ function calculateDelay(attempt, retryConfig) { if (retryConfig.strategy === "exponential") { const delay = retryConfig.initialDelay * Math.pow(retryConfig.factor, attempt); return Math.min(delay, retryConfig.maxDelay); } // Can add other strategies here if needed return retryConfig.initialDelay; } function isRetryableError(e) { if(e instanceof ResponseError) { if(((e.responseCode >= 500 && e.responseCode <= 600) || e.responseCode == 429 || e.responseCode == 408)) { return true; } } else if(e instanceof RequestError) { //should include TimeoutError return true; } return false; } /** * Execute a fetch request with retry logic * @param {string} url - the URL to fetch * @param {Object} request - fetch request options * @param {Object} options - additional options * @param {string} options.requestName - name of the request for logging * @param {string} options.correlationId - correlation ID for tracing * @param {Object} retryConfig - retry configuration * @returns {Promise<Response>} - fetch response */ async function fetchWithRetry(url, request, options) { const { requestName, correlationId } = options; const retryConfig = request.retry; const retryErrors = []; const requestWithName = {...request, name: requestName}; for (let attempt = 0; attempt <= retryConfig.retries; attempt++) { try { // If not the first attempt, log retry information if (attempt > 0) { LOG.info(`Retry attempt ${attempt}/${retryConfig.retries} for [${requestName}] to ${url}`, { correlationId }); } return await fetchWithoutRetry(url, request, options); } catch (e) { if(!isRetryableError(e)) { throw e; } // Add error to retry errors collection retryErrors.push(e); // If this was the last attempt, don't retry if (attempt >= retryConfig.retries) { break; } // Calculate and wait for the delay before next retry const delay = calculateDelay(attempt, retryConfig); LOG.debug(`Waiting ${delay}ms before next retry`, { correlationId }); await sleep(delay); } } const error= new RetryError(url, requestWithName, retryErrors); // Log detailed retry information LOG.error(error.message, { correlationId }); throw error; } async function fetchWithoutRetry(url, request, { requestName, correlationId, extractHeaders }) { if(correlationId) { request.headers[CORRELATIONID_HEADER_VCAP] = correlationId; } LOG.debug(`HTTP request [${requestName}] to ${url} with options`, {correlationId, ...shrinkRequestOptionsForLog(request)}); let response; try { // Use the new fetchWithRetry function instead of direct fetch response = await fetch(url, request); } catch(e) { const requestWithName = {...request, name: requestName}; const error = e.code === 'ETIMEDOUT' ? new TimeoutError(url, requestWithName, e) : new RequestError(url, requestWithName, e); LOG.error(error.message, { correlationId }); throw error; } if(!response.ok) { const responseText = await response.text(); const error = new ResponseError(url, {...request, name: requestName}, response.status, responseText); LOG.error(error.message, {correlationId}); throw error; } const json = await response.json(); LOG.debug(`HTTP response [${requestName}]:`, {correlationId, ...json}); if(extractHeaders != null) { json[extractHeaders] = response.headers; } return json; } async function jsonRequest(url, request, options = {}) { // Use default retry configuration if not provided Object.assign(request, { redirect: "error", follow: 0, timeout: Math.min(MAX_TIMEOUT, request.timeout ?? DEFAULT_TIMEOUT), }); request.headers ??= {}; Object.assign(request.headers, { Accept: 'application/json', 'User-Agent': USER_AGENT }); if(request.retry) { return fetchWithRetry(url, request, options); } else { return fetchWithoutRetry(url, request, options); } } module.exports = { jsonRequest }