refactor(api): tighten relay retry policy and config safety bounds
This commit is contained in:
@@ -13,6 +13,11 @@ 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 MAX_TIMEOUT_MS = 30000;
|
||||
const MAX_RETRIES = 4;
|
||||
const MAX_RETRY_DELAY_MS = 5000;
|
||||
|
||||
const NON_RETRYABLE_STATUS_CODES = new Set([400, 401, 403, 404, 422]);
|
||||
|
||||
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
||||
const RETRYABLE_ERROR_CODES = new Set([
|
||||
@@ -29,24 +34,66 @@ const parsePositiveInt = (value, fallback) => {
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
|
||||
const clamp = (value, min, max) => {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
};
|
||||
|
||||
const sanitizeNumberConfig = ({
|
||||
value,
|
||||
fallback,
|
||||
min,
|
||||
max,
|
||||
allowZero = false
|
||||
}) => {
|
||||
const parsed = parsePositiveInt(value, fallback);
|
||||
const lowerBound = allowZero ? 0 : min;
|
||||
return clamp(parsed, lowerBound, max);
|
||||
};
|
||||
|
||||
const resolveNumericOverride = ({
|
||||
overrideValue,
|
||||
fallbackValue,
|
||||
min,
|
||||
max,
|
||||
allowZero = false
|
||||
}) => {
|
||||
if (typeof overrideValue !== "number" || !Number.isFinite(overrideValue)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
const lowerBound = allowZero ? 0 : min;
|
||||
return clamp(Math.floor(overrideValue), lowerBound, max);
|
||||
};
|
||||
|
||||
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
|
||||
)
|
||||
timeoutMs: sanitizeNumberConfig({
|
||||
value: process.env.RELAY_TIMEOUT_MS,
|
||||
fallback: DEFAULT_TIMEOUT_MS,
|
||||
min: 100,
|
||||
max: MAX_TIMEOUT_MS
|
||||
}),
|
||||
maxRetries: sanitizeNumberConfig({
|
||||
value: process.env.RELAY_RETRY_MAX,
|
||||
fallback: DEFAULT_MAX_RETRIES,
|
||||
min: 0,
|
||||
max: MAX_RETRIES,
|
||||
allowZero: true
|
||||
}),
|
||||
retryBaseDelayMs: sanitizeNumberConfig({
|
||||
value: process.env.RELAY_RETRY_BASE_DELAY_MS,
|
||||
fallback: DEFAULT_RETRY_BASE_DELAY_MS,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
}),
|
||||
retryMaxDelayMs: sanitizeNumberConfig({
|
||||
value: process.env.RELAY_RETRY_MAX_DELAY_MS,
|
||||
fallback: DEFAULT_RETRY_MAX_DELAY_MS,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
@@ -66,7 +113,9 @@ const shouldRetryRelayError = ({ error, attempt, maxRetries }) => {
|
||||
if (attempt >= maxRetries) return false;
|
||||
|
||||
const status = error?.response?.status;
|
||||
if (NON_RETRYABLE_STATUS_CODES.has(status)) return false;
|
||||
if (RETRYABLE_STATUS_CODES.has(status)) return true;
|
||||
if (typeof status === "number") return false;
|
||||
|
||||
const code = error?.code;
|
||||
return RETRYABLE_ERROR_CODES.has(code);
|
||||
@@ -92,7 +141,9 @@ export const relayGet = async ({
|
||||
: data
|
||||
);
|
||||
} catch (error) {
|
||||
consoleLogger(error);
|
||||
if (!error?.__relayAlreadyLogged) {
|
||||
consoleLogger(error);
|
||||
}
|
||||
return respondError(res, errorResponse);
|
||||
}
|
||||
};
|
||||
@@ -122,25 +173,36 @@ export const forwardGetData = async ({
|
||||
}) => {
|
||||
const relayConfig = getRelayConfig();
|
||||
|
||||
const resolvedTimeoutMs =
|
||||
typeof timeoutMs === "number" && timeoutMs >= 0
|
||||
? timeoutMs
|
||||
: relayConfig.timeoutMs;
|
||||
const resolvedTimeoutMs = resolveNumericOverride({
|
||||
overrideValue: timeoutMs,
|
||||
fallbackValue: relayConfig.timeoutMs,
|
||||
min: 100,
|
||||
max: MAX_TIMEOUT_MS
|
||||
});
|
||||
|
||||
const resolvedMaxRetries =
|
||||
typeof maxRetries === "number" && maxRetries >= 0
|
||||
? maxRetries
|
||||
: relayConfig.maxRetries;
|
||||
const resolvedMaxRetries = resolveNumericOverride({
|
||||
overrideValue: maxRetries,
|
||||
fallbackValue: relayConfig.maxRetries,
|
||||
min: 0,
|
||||
max: MAX_RETRIES,
|
||||
allowZero: true
|
||||
});
|
||||
|
||||
const resolvedRetryBaseDelayMs =
|
||||
typeof retryBaseDelayMs === "number" && retryBaseDelayMs >= 0
|
||||
? retryBaseDelayMs
|
||||
: relayConfig.retryBaseDelayMs;
|
||||
const resolvedRetryBaseDelayMs = resolveNumericOverride({
|
||||
overrideValue: retryBaseDelayMs,
|
||||
fallbackValue: relayConfig.retryBaseDelayMs,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
});
|
||||
|
||||
const resolvedRetryMaxDelayMs =
|
||||
typeof retryMaxDelayMs === "number" && retryMaxDelayMs >= 0
|
||||
? retryMaxDelayMs
|
||||
: relayConfig.retryMaxDelayMs;
|
||||
const resolvedRetryMaxDelayMs = resolveNumericOverride({
|
||||
overrideValue: retryMaxDelayMs,
|
||||
fallbackValue: relayConfig.retryMaxDelayMs,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
});
|
||||
|
||||
const tokenAccessToken =
|
||||
typeof accessToken === "string" && accessToken.length > 0
|
||||
@@ -184,6 +246,7 @@ export const forwardGetData = async ({
|
||||
});
|
||||
|
||||
if (!retryEligible) {
|
||||
error.__relayAlreadyLogged = true;
|
||||
structuredRelayLog("relay_request_failed", {
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
|
||||
Reference in New Issue
Block a user