fix: expose actionable API error details

This commit is contained in:
2026-08-09 03:26:45 +08:00
parent 6dc9052219
commit b1ac6cef86
4 changed files with 227 additions and 9 deletions
+53 -4
View File
@@ -43,22 +43,71 @@ function createIdempotencyKey() {
].join("-");
}
function collectApiErrorMessages(value, messages = []) {
if (Array.isArray(value)) {
value.forEach((item) => collectApiErrorMessages(item, messages));
} else if (value && typeof value === "object") {
Object.values(value).forEach((item) => collectApiErrorMessages(item, messages));
} else if (value !== undefined && value !== null) {
const message = String(value).trim();
if (message && !messages.includes(message)) messages.push(message);
}
return messages;
}
function logApiError(error) {
console.error("[Hulumath API]", {
method: error.method,
path: error.path,
status: error.status,
code: error.code,
requestId: error.requestId,
details: error.details,
});
}
async function api(path, options = {}) {
const method = (options.method || "GET").toUpperCase();
const requestPath = `/api/v1/${path}`;
const headers = { Accept: "application/json", ...(options.headers || {}) };
if (options.body && typeof options.body !== "string") {
headers["Content-Type"] = "application/json";
options.body = JSON.stringify(options.body);
}
if (!["GET", "HEAD"].includes(options.method || "GET")) headers["X-CSRFToken"] = csrfToken();
const response = await fetch(`/api/v1/${path}`, { credentials: "same-origin", ...options, headers });
if (!["GET", "HEAD"].includes(method)) headers["X-CSRFToken"] = csrfToken();
let response;
try {
response = await fetch(requestPath, { credentials: "same-origin", ...options, headers });
} catch (cause) {
const error = new Error("网络连接失败,请检查连接后重试");
error.status = null;
error.code = "network_error";
error.requestId = null;
error.details = null;
error.path = requestPath;
error.method = method;
error.cause = cause;
logApiError(error);
throw error;
}
if (response.status === 204) return null;
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const details = payload.error?.details;
const message = payload.error?.message || payload.detail ||
(details ? Object.values(details).flat().join(" ") : "请求失败");
const detailMessages = collectApiErrorMessages(details);
const requestId = payload.error?.request_id || response.headers.get("X-Request-ID");
const baseMessage = detailMessages.length
? detailMessages.join("")
: payload.error?.message || payload.detail || `请求失败(HTTP ${response.status}`;
const message = requestId ? `${baseMessage}(请求编号:${requestId}` : baseMessage;
const error = new Error(message);
error.status = response.status;
error.code = payload.error?.code || "request_error";
error.requestId = requestId;
error.details = details || null;
error.path = requestPath;
error.method = method;
logApiError(error);
throw error;
}
return payload;