241 lines
6.0 KiB
JavaScript
241 lines
6.0 KiB
JavaScript
const DROP_SENSITIVE = Object.freeze([
|
|
"cause",
|
|
"body",
|
|
"headers",
|
|
"authorization",
|
|
"url",
|
|
"query",
|
|
"stack",
|
|
"storageValue",
|
|
]);
|
|
|
|
/**
|
|
* @typedef {{
|
|
* kind: string,
|
|
* defaultRetryable: boolean,
|
|
* severity: string,
|
|
* userMessageKey: string,
|
|
* action: string,
|
|
* telemetryEvent: string,
|
|
* redaction: readonly string[]
|
|
* }} ErrorDefinition
|
|
*/
|
|
|
|
/**
|
|
* @param {string} kind
|
|
* @param {boolean} defaultRetryable
|
|
* @param {string} severity
|
|
* @param {string} action
|
|
* @param {string} [telemetryEvent]
|
|
* @returns {Readonly<ErrorDefinition>}
|
|
*/
|
|
const row = (
|
|
kind,
|
|
defaultRetryable,
|
|
severity,
|
|
action,
|
|
telemetryEvent = "api.request.failed",
|
|
) =>
|
|
Object.freeze({
|
|
kind,
|
|
defaultRetryable,
|
|
severity,
|
|
userMessageKey: `error.${kind.toLowerCase()}`,
|
|
action,
|
|
telemetryEvent,
|
|
redaction: DROP_SENSITIVE,
|
|
});
|
|
|
|
export const ERROR_REGISTRY = Object.freeze({
|
|
NETWORK_UNREACHABLE: row("NETWORK_UNREACHABLE", true, "warning", "retry"),
|
|
REQUEST_TIMEOUT: row("REQUEST_TIMEOUT", true, "warning", "retry"),
|
|
REQUEST_ABORTED: row("REQUEST_ABORTED", false, "info", "none"),
|
|
CONTENT_TYPE_MISMATCH: row(
|
|
"CONTENT_TYPE_MISMATCH",
|
|
false,
|
|
"error",
|
|
"contact-support",
|
|
),
|
|
MALFORMED_JSON: row("MALFORMED_JSON", false, "error", "contact-support"),
|
|
ENVELOPE_MISMATCH: row("ENVELOPE_MISMATCH", false, "error", "contact-support"),
|
|
SCHEMA_MISMATCH: row("SCHEMA_MISMATCH", false, "error", "contact-support"),
|
|
AUTH_REQUIRED: row("AUTH_REQUIRED", false, "info", "reauth"),
|
|
AUTH_INTEGRATION_FAILURE: row(
|
|
"AUTH_INTEGRATION_FAILURE",
|
|
false,
|
|
"error",
|
|
"contact-support",
|
|
),
|
|
FORBIDDEN: row("FORBIDDEN", false, "warning", "navigate"),
|
|
NOT_FOUND: row("NOT_FOUND", false, "info", "navigate"),
|
|
CONFLICT: row("CONFLICT", false, "warning", "retry"),
|
|
VALIDATION_REJECTED: row("VALIDATION_REJECTED", false, "info", "none"),
|
|
UNKNOWN_CLIENT_FAILURE: row(
|
|
"UNKNOWN_CLIENT_FAILURE",
|
|
false,
|
|
"warning",
|
|
"contact-support",
|
|
),
|
|
RATE_LIMITED: row("RATE_LIMITED", true, "warning", "retry"),
|
|
SERVER_FAILURE: row("SERVER_FAILURE", true, "error", "retry"),
|
|
CHUNK_LOAD_FAILURE: row(
|
|
"CHUNK_LOAD_FAILURE",
|
|
false,
|
|
"error",
|
|
"reload-once",
|
|
"release.mismatch.detected",
|
|
),
|
|
BOOT_CONFIG_FAILURE: row(
|
|
"BOOT_CONFIG_FAILURE",
|
|
false,
|
|
"error",
|
|
"contact-support",
|
|
"app.boot.failed",
|
|
),
|
|
RELEASE_MANIFEST_FAILURE: row(
|
|
"RELEASE_MANIFEST_FAILURE",
|
|
false,
|
|
"error",
|
|
"contact-support",
|
|
"app.boot.failed",
|
|
),
|
|
DEPLOY_MISMATCH: row(
|
|
"DEPLOY_MISMATCH",
|
|
false,
|
|
"error",
|
|
"reload-once",
|
|
"release.mismatch.detected",
|
|
),
|
|
STORAGE_UNAVAILABLE: row(
|
|
"STORAGE_UNAVAILABLE",
|
|
false,
|
|
"warning",
|
|
"none",
|
|
"storage.operation.failed",
|
|
),
|
|
STORAGE_QUOTA_EXCEEDED: row(
|
|
"STORAGE_QUOTA_EXCEEDED",
|
|
false,
|
|
"warning",
|
|
"none",
|
|
"storage.operation.failed",
|
|
),
|
|
RENDER_FAILURE: row(
|
|
"RENDER_FAILURE",
|
|
false,
|
|
"error",
|
|
"reload-once",
|
|
"ui.render.failed",
|
|
),
|
|
TELEMETRY_FAILURE: row(
|
|
"TELEMETRY_FAILURE",
|
|
false,
|
|
"info",
|
|
"none",
|
|
"telemetry.delivery.dropped",
|
|
),
|
|
QUERY_CACHE_FAILURE: row(
|
|
"QUERY_CACHE_FAILURE",
|
|
false,
|
|
"error",
|
|
"retry",
|
|
"query.cache.failed",
|
|
),
|
|
UNKNOWN_FAILURE: row("UNKNOWN_FAILURE", false, "error", "contact-support"),
|
|
});
|
|
|
|
/**
|
|
* @typedef {{
|
|
* kind: string,
|
|
* code: string,
|
|
* httpStatus?: number,
|
|
* retryable: boolean,
|
|
* operationId: string,
|
|
* attemptCount: number,
|
|
* requestId?: string,
|
|
* traceId?: string,
|
|
* retryAfterMs?: number,
|
|
* userMessageKey: string,
|
|
* action: string,
|
|
* causeClass?: string
|
|
* }} ApiFailure
|
|
*/
|
|
|
|
/**
|
|
* @param {string} kind
|
|
* @param {string} operationId
|
|
* @param {number} attempt
|
|
* @param {{
|
|
* code?: string,
|
|
* httpStatus?: number,
|
|
* requestId?: string,
|
|
* traceId?: string,
|
|
* retryAfterMs?: number,
|
|
* causeClass?: string
|
|
* }} [details]
|
|
* @returns {ApiFailure}
|
|
*/
|
|
export function createFailure(kind, operationId, attempt, details = {}) {
|
|
const registry =
|
|
/** @type {Readonly<Record<string, Readonly<ErrorDefinition>>>} */ (
|
|
ERROR_REGISTRY
|
|
);
|
|
const definition =
|
|
registry[kind] ?? ERROR_REGISTRY.UNKNOWN_FAILURE;
|
|
return Object.freeze({
|
|
kind: definition.kind,
|
|
code: typeof details.code === "string" ? details.code : definition.kind,
|
|
retryable: definition.defaultRetryable,
|
|
operationId,
|
|
attemptCount: Math.max(1, attempt + 1),
|
|
...(Number.isInteger(details.httpStatus)
|
|
? { httpStatus: details.httpStatus }
|
|
: {}),
|
|
...(typeof details.requestId === "string" ? { requestId: details.requestId } : {}),
|
|
...(typeof details.traceId === "string" ? { traceId: details.traceId } : {}),
|
|
...(typeof details.retryAfterMs === "number"
|
|
? { retryAfterMs: details.retryAfterMs }
|
|
: {}),
|
|
...(typeof details.causeClass === "string"
|
|
? { causeClass: details.causeClass }
|
|
: {}),
|
|
userMessageKey: definition.userMessageKey,
|
|
action: definition.action,
|
|
});
|
|
}
|
|
|
|
/** @param {number} status */
|
|
export function kindForStatus(status) {
|
|
if (status === 401) return "AUTH_REQUIRED";
|
|
if (status === 403) return "FORBIDDEN";
|
|
if (status === 404) return "NOT_FOUND";
|
|
if (status === 409) return "CONFLICT";
|
|
if (status === 422) return "VALIDATION_REJECTED";
|
|
if (status === 429) return "RATE_LIMITED";
|
|
if (status >= 500) return "SERVER_FAILURE";
|
|
if (status >= 400) return "UNKNOWN_CLIENT_FAILURE";
|
|
return "ENVELOPE_MISMATCH";
|
|
}
|
|
|
|
/**
|
|
* Total catch-all that intentionally discards the thrown value.
|
|
*
|
|
* @param {unknown} value
|
|
* @param {{ operationId?: string, attempt?: number }} [context]
|
|
*/
|
|
export function normalizeUnknownFailure(value, context = {}) {
|
|
const causeClass =
|
|
value instanceof Error
|
|
? value.name
|
|
: value === null
|
|
? "null"
|
|
: typeof value;
|
|
|
|
return createFailure(
|
|
"UNKNOWN_FAILURE",
|
|
context.operationId ?? "UNKNOWN_OPERATION",
|
|
context.attempt ?? 0,
|
|
{ code: "UNKNOWN_FAILURE", causeClass },
|
|
);
|
|
}
|