222 lines
6.1 KiB
JavaScript
222 lines
6.1 KiB
JavaScript
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;
|
|
};
|