@sap/xssec
Version:
XS Advanced Container Security API for node.js
164 lines (138 loc) • 5.42 kB
JavaScript
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
}