fix: include actionable error details

This commit is contained in:
JetSprow
2026-04-29 15:03:00 +10:00
parent d7681240bb
commit df74723b52
28 changed files with 178 additions and 74 deletions

View File

@@ -12,7 +12,7 @@ export async function authenticateAgent(
): Promise<{ nodeId: string } | NextResponse> {
const authHeader = req.headers.get("authorization") || "";
if (!authHeader.startsWith("Bearer ")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
return NextResponse.json({ error: "认证失败:请求头缺少 Bearer Token" }, { status: 401 });
}
const token = authHeader.slice(7);
@@ -38,7 +38,7 @@ export async function authenticateAgent(
}
}
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
return NextResponse.json({ error: "认证失败Token 无效、已撤销或节点未配置探测 Token" }, { status: 401 });
}
/** Type guard: true when authenticateAgent returned an error response */

View File

@@ -5,7 +5,7 @@ const ALGORITHM = "aes-256-gcm";
function getKey() {
const raw = process.env.ENCRYPTION_KEY;
if (!raw || Buffer.byteLength(raw, "utf-8") < 32) {
throw new Error("ENCRYPTION_KEY must be at least 32 bytes");
throw new Error("加密配置错误:ENCRYPTION_KEY 未配置或长度不足 32 字节");
}
return Buffer.from(raw, "utf-8").subarray(0, 32);
}
@@ -21,7 +21,7 @@ export function encrypt(text: string): string {
export function decrypt(data: string): string {
const parts = data.split(":");
if (parts.length !== 3) {
throw new Error("Invalid encrypted data format");
throw new Error("解密失败:加密数据格式不正确,期望 iv:authTag:ciphertext 三段内容");
}
const [ivHex, authTagHex, encryptedHex] = parts;
const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), Buffer.from(ivHex, "hex"));

View File

@@ -1,14 +1,94 @@
type ErrorLikeRecord = Record<string, unknown>;
const REDACTED_SERVER_COMPONENT_ERROR = /An error occurred in the Server Components render/i;
const DETAIL_KEYS = ["error", "message", "detail", "details", "reason", "description"];
const CODE_KEYS = ["code", "status", "statusCode", "digest"];
function normalizeMessage(message: string): string | null {
const trimmed = message.trim();
if (!trimmed) return null;
if (REDACTED_SERVER_COMPONENT_ERROR.test(trimmed)) {
return "服务端渲染时发生异常,生产环境已隐藏原始堆栈";
}
return trimmed;
}
function isRecord(value: unknown): value is ErrorLikeRecord {
return value != null && typeof value === "object" && !Array.isArray(value);
}
function pushUnique(messages: string[], value: unknown) {
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return;
const normalized = normalizeMessage(String(value));
if (normalized && !messages.includes(normalized)) messages.push(normalized);
}
function collectFromArray(messages: string[], value: unknown) {
if (!Array.isArray(value)) return;
for (const item of value) {
if (typeof item === "string") {
pushUnique(messages, item);
continue;
}
if (!isRecord(item)) continue;
const path = Array.isArray(item.path) ? item.path.join(".") : null;
const message = typeof item.message === "string" ? item.message : null;
if (message) pushUnique(messages, path ? `${path}${message}` : message);
}
}
function collectErrorMessages(error: unknown, messages: string[], seen = new WeakSet<object>()) {
if (error instanceof Error) {
pushUnique(messages, error.message);
const digest = (error as Error & { digest?: unknown }).digest;
if (digest) pushUnique(messages, `错误编号:${String(digest)}`);
if (error.cause) collectErrorMessages(error.cause, messages, seen);
return;
}
if (typeof error === "string") {
pushUnique(messages, error);
return;
}
if (!isRecord(error)) return;
if (seen.has(error)) return;
seen.add(error);
for (const key of DETAIL_KEYS) {
const value = error[key];
if (typeof value === "string") {
pushUnique(messages, value);
} else if (Array.isArray(value)) {
collectFromArray(messages, value);
} else if (isRecord(value)) {
collectErrorMessages(value, messages, seen);
}
}
for (const key of CODE_KEYS) {
const value = error[key];
if (value == null || value === "") continue;
const label = key === "digest" ? "错误编号" : key;
pushUnique(messages, `${label}${String(value)}`);
}
}
export function getErrorMessage(
error: unknown,
fallback = "操作失败",
): string {
if (error instanceof Error && error.message.trim()) {
return error.message.trim();
const messages: string[] = [];
collectErrorMessages(error, messages);
if (messages.length > 0) {
return messages.join("");
}
if (typeof error === "string" && error.trim()) {
return error.trim();
}
return fallback;
const fallbackMessage = normalizeMessage(fallback) ?? "操作失败";
const errorType = error === null ? "null" : typeof error;
return `${fallbackMessage}:请求没有返回可读错误内容(错误类型:${errorType}),请查看服务端日志定位原因。`;
}

View File

@@ -6,7 +6,8 @@ function extractApiError(payload: unknown): string | null {
}
const error = (payload as { error?: unknown }).error;
return typeof error === "string" && error.trim() ? error.trim() : null;
if (typeof error === "string" && error.trim()) return error.trim();
return getErrorMessage(payload, "请求失败");
}
export async function fetchJson<T>(