142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
export const HWLAB_ERROR_DIAGNOSTIC_VERSION = "hwlab-error-diagnostic-v1";
|
|
|
|
export function decorateErrorPayloadForHttp(payload, options = {}) {
|
|
const statusCode = numericStatus(options.statusCode);
|
|
if (!statusCode || statusCode < 400 || !plainObject(payload) || !("error" in payload)) return payload;
|
|
|
|
const context = options.context ?? options.response?.hwlabHttpRequestContext ?? options.request?.hwlabHttpRequestContext ?? {};
|
|
const originalError = normalizeErrorObject(payload.error, payload);
|
|
if (!originalError) return payload;
|
|
|
|
const rawCode = originalError.code ?? payload.code ?? payload.error ?? statusCodeToCode(statusCode);
|
|
const diagnosticCode = safeCode(rawCode);
|
|
const errorCode = typeof originalError.code === "number" ? originalError.code : diagnosticCode;
|
|
const message = firstText(originalError.message, payload.message, diagnosticCode);
|
|
const userMessage = firstText(originalError.userMessage, payload.userMessage, message);
|
|
const diagnostic = {
|
|
contractVersion: HWLAB_ERROR_DIAGNOSTIC_VERSION,
|
|
traceId: safeOtelTraceId(context.traceId) ?? safeOtelTraceId(originalError.diagnostic?.traceId) ?? safeOtelTraceId(originalError.otelTraceId) ?? safeOtelTraceId(payload.otelTraceId) ?? null,
|
|
requestId: safeRequestId(context.requestId) ?? safeRequestId(originalError.diagnostic?.requestId) ?? safeRequestId(originalError.requestId) ?? safeRequestId(payload.requestId) ?? null,
|
|
serviceId: safeToken(originalError.serviceId ?? payload.serviceId ?? options.serviceId, "hwlab-cloud-api"),
|
|
route: safeRoute(originalError.route ?? payload.route ?? options.route ?? context.route),
|
|
layer: safeToken(originalError.layer ?? options.layer, "api"),
|
|
category: safeToken(originalError.category ?? options.category, categoryForStatus(statusCode)),
|
|
code: diagnosticCode,
|
|
httpStatus: statusCode,
|
|
source: safeToken(options.source, "server"),
|
|
observedAt: new Date().toISOString(),
|
|
valuesPrinted: false
|
|
};
|
|
const nextError = {
|
|
...originalError,
|
|
code: errorCode,
|
|
message,
|
|
userMessage,
|
|
diagnostic: {
|
|
...(plainObject(originalError.diagnostic) ? originalError.diagnostic : {}),
|
|
...diagnostic
|
|
},
|
|
valuesPrinted: originalError.valuesPrinted === true ? true : false
|
|
};
|
|
return { ...payload, error: nextError };
|
|
}
|
|
|
|
export function logErrorEnvelope(payload, options = {}) {
|
|
const statusCode = numericStatus(options.statusCode);
|
|
if (!statusCode || statusCode < 400 || !plainObject(payload) || !plainObject(payload.error)) return;
|
|
const diagnostic = plainObject(payload.error.diagnostic) ? payload.error.diagnostic : null;
|
|
if (!diagnostic) return;
|
|
const record = {
|
|
event: "hwlab.error",
|
|
statusCode,
|
|
trace_id: diagnostic.traceId ?? null,
|
|
request_id: diagnostic.requestId ?? null,
|
|
route: diagnostic.route ?? null,
|
|
serviceId: diagnostic.serviceId ?? "hwlab-cloud-api",
|
|
error: {
|
|
code: payload.error.code ?? diagnostic.code ?? "unknown_error",
|
|
layer: payload.error.layer ?? diagnostic.layer ?? "api",
|
|
category: payload.error.category ?? diagnostic.category ?? categoryForStatus(statusCode),
|
|
retryable: payload.error.retryable ?? null
|
|
},
|
|
valuesPrinted: false
|
|
};
|
|
const logger = options.logger ?? console;
|
|
const line = JSON.stringify(record);
|
|
if (statusCode >= 500 && typeof logger.error === "function") {
|
|
logger.error(line);
|
|
} else if (typeof logger.warn === "function") {
|
|
logger.warn(line);
|
|
}
|
|
}
|
|
|
|
function normalizeErrorObject(value, payload) {
|
|
if (plainObject(value)) return { ...value };
|
|
const text = firstText(value, payload?.message, "unknown_error");
|
|
return {
|
|
code: safeCode(text),
|
|
message: text,
|
|
userMessage: text
|
|
};
|
|
}
|
|
|
|
function numericStatus(value) {
|
|
const status = Number(value);
|
|
return Number.isInteger(status) && status >= 100 && status <= 599 ? status : null;
|
|
}
|
|
|
|
function statusCodeToCode(statusCode) {
|
|
if (statusCode === 400) return "bad_request";
|
|
if (statusCode === 401) return "auth_required";
|
|
if (statusCode === 403) return "forbidden";
|
|
if (statusCode === 404) return "not_found";
|
|
if (statusCode === 409) return "conflict";
|
|
if (statusCode === 429) return "rate_limited";
|
|
if (statusCode >= 500) return "internal_error";
|
|
return "error";
|
|
}
|
|
|
|
function categoryForStatus(statusCode) {
|
|
if (statusCode >= 500) return "server";
|
|
if (statusCode >= 400) return "client";
|
|
return "error";
|
|
}
|
|
|
|
function firstText(...values) {
|
|
for (const value of values) {
|
|
const text = String(value ?? "").trim();
|
|
if (text) return text;
|
|
}
|
|
return "unknown_error";
|
|
}
|
|
|
|
function safeCode(value) {
|
|
const text = firstText(value).toLowerCase().replace(/[^a-z0-9_.:-]+/gu, "_").replace(/^_+|_+$/gu, "");
|
|
return text.slice(0, 96) || "unknown_error";
|
|
}
|
|
|
|
function safeToken(value, fallback) {
|
|
const text = String(value ?? "").trim();
|
|
return /^[A-Za-z0-9_.:-]{1,96}$/u.test(text) ? text : fallback;
|
|
}
|
|
|
|
function safeRoute(value) {
|
|
const text = String(value ?? "").trim().split("?")[0] || "/";
|
|
if (!text.startsWith("/")) return "/";
|
|
return text.slice(0, 180);
|
|
}
|
|
|
|
function safeRequestId(value) {
|
|
const text = String(value ?? "").trim();
|
|
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
|
|
}
|
|
|
|
function safeOtelTraceId(value) {
|
|
const text = String(value ?? "").trim().toLowerCase();
|
|
return /^[0-9a-f]{32}$/u.test(text) && text !== "00000000000000000000000000000000" ? text : null;
|
|
}
|
|
|
|
function plainObject(value) {
|
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
}
|