import axios from "axios"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger, redactSensitive } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "./apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; const DEFAULT_TIMEOUT_MS = 8000; const DEFAULT_MAX_RETRIES = 2; const DEFAULT_RETRY_BASE_DELAY_MS = 200; const DEFAULT_RETRY_MAX_DELAY_MS = 1200; const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]); const RETRYABLE_ERROR_CODES = new Set([ "ECONNABORTED", "ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "EPIPE" ]); const parsePositiveInt = (value, fallback) => { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; }; const getRelayConfig = () => { return { timeoutMs: parsePositiveInt( process.env.RELAY_TIMEOUT_MS, DEFAULT_TIMEOUT_MS ), maxRetries: parsePositiveInt( process.env.RELAY_RETRY_MAX, DEFAULT_MAX_RETRIES ), retryBaseDelayMs: parsePositiveInt( process.env.RELAY_RETRY_BASE_DELAY_MS, DEFAULT_RETRY_BASE_DELAY_MS ), retryMaxDelayMs: parsePositiveInt( process.env.RELAY_RETRY_MAX_DELAY_MS, DEFAULT_RETRY_MAX_DELAY_MS ) }; }; const backoffDelayMs = ({ attempt, retryBaseDelayMs, retryMaxDelayMs }) => { return Math.min(retryMaxDelayMs, retryBaseDelayMs * 2 ** attempt); }; const wait = async (delayMs) => { await new Promise((resolve) => setTimeout(resolve, delayMs)); }; const structuredRelayLog = (event, payload) => { console.info(event, redactSensitive(payload)); }; const shouldRetryRelayError = ({ error, attempt, maxRetries }) => { if (attempt >= maxRetries) return false; const status = error?.response?.status; if (RETRYABLE_STATUS_CODES.has(status)) return true; const code = error?.code; return RETRYABLE_ERROR_CODES.has(code); }; export const relayGet = async ({ queryUrl, res, errorResponse, transformData, requestOptionsBuilder }) => { try { const { data, accessToken } = await relayGetData({ queryUrl, requestOptionsBuilder }); return respondSuccess( res, typeof transformData === "function" ? await transformData(data, accessToken) : data ); } catch (error) { consoleLogger(error); return respondError(res, errorResponse); } }; export const relayGetData = async ({ queryUrl, requestOptionsBuilder, accessToken }) => { return forwardGetData({ queryUrl, requestOptionsBuilder, accessToken }); }; export const forwardGetData = async ({ baseUrl, queryUrl, requestOptionsBuilder, accessToken, appendHash = true, timeoutMs, maxRetries, retryBaseDelayMs, retryMaxDelayMs }) => { const relayConfig = getRelayConfig(); const resolvedTimeoutMs = typeof timeoutMs === "number" && timeoutMs >= 0 ? timeoutMs : relayConfig.timeoutMs; const resolvedMaxRetries = typeof maxRetries === "number" && maxRetries >= 0 ? maxRetries : relayConfig.maxRetries; const resolvedRetryBaseDelayMs = typeof retryBaseDelayMs === "number" && retryBaseDelayMs >= 0 ? retryBaseDelayMs : relayConfig.retryBaseDelayMs; const resolvedRetryMaxDelayMs = typeof retryMaxDelayMs === "number" && retryMaxDelayMs >= 0 ? retryMaxDelayMs : relayConfig.retryMaxDelayMs; const tokenAccessToken = typeof accessToken === "string" && accessToken.length > 0 ? accessToken : (await getToken()).access_token; const resolvedBaseUrl = baseUrl || WEBAPI_URL; const finalUrl = resolvedBaseUrl + queryUrl + (appendHash ? hashAPIPath(queryUrl) : ""); const requestOptions = typeof requestOptionsBuilder === "function" ? requestOptionsBuilder(tokenAccessToken) : azureHeaders(tokenAccessToken); const axiosOptions = { ...(requestOptions || {}), timeout: requestOptions?.timeout != null ? requestOptions.timeout : resolvedTimeoutMs }; let lastError; for (let attempt = 0; attempt <= resolvedMaxRetries; attempt += 1) { try { const { data } = await axios.get(finalUrl, axiosOptions); return { data, accessToken: tokenAccessToken }; } catch (error) { lastError = error; const retryEligible = shouldRetryRelayError({ error, attempt, maxRetries: resolvedMaxRetries }); if (!retryEligible) { structuredRelayLog("relay_request_failed", { queryUrl, baseUrl: resolvedBaseUrl, attempt: attempt + 1, maxAttempts: resolvedMaxRetries + 1, status: error?.response?.status, code: error?.code, message: error?.message }); throw error; } const delayMs = backoffDelayMs({ attempt, retryBaseDelayMs: resolvedRetryBaseDelayMs, retryMaxDelayMs: resolvedRetryMaxDelayMs }); structuredRelayLog("relay_request_retrying", { queryUrl, baseUrl: resolvedBaseUrl, attempt: attempt + 1, maxAttempts: resolvedMaxRetries + 1, delayMs, status: error?.response?.status, code: error?.code, message: error?.message }); await wait(delayMs); } } throw lastError; };