You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
111 lines
2.8 KiB
111 lines
2.8 KiB
import { NextResponse } from "next/server";
|
|
|
|
export type ApiErrorCode =
|
|
| "AUTH_REQUIRED"
|
|
| "VALIDATION_ERROR"
|
|
| "CONFIG_ERROR"
|
|
| "PROVIDER_ERROR"
|
|
| "RATE_LIMITED"
|
|
| "INTERNAL_ERROR";
|
|
|
|
export function isAuthRequiredMessage(message: string): boolean {
|
|
const normalized = message.trim().toLowerCase();
|
|
if (!normalized) return false;
|
|
|
|
return (
|
|
normalized.includes("please login") ||
|
|
normalized.includes("please log in") ||
|
|
normalized.includes("login first") ||
|
|
normalized.includes("log in first") ||
|
|
normalized.includes("not logged in") ||
|
|
normalized.includes("session expired") ||
|
|
normalized.includes("token expired") ||
|
|
message.includes("请先登录") ||
|
|
message.includes("請先登入") ||
|
|
message.includes("未登录") ||
|
|
message.includes("未登入") ||
|
|
message.includes("登录已过期") ||
|
|
message.includes("登入已過期")
|
|
);
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
readonly status: number;
|
|
readonly code: ApiErrorCode;
|
|
|
|
constructor(status: number, code: ApiErrorCode, message: string) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
this.status = status;
|
|
this.code = code;
|
|
}
|
|
|
|
static unauthorized(message = "请先登录"): ApiError {
|
|
return new ApiError(401, "AUTH_REQUIRED", message);
|
|
}
|
|
|
|
static badRequest(message: string): ApiError {
|
|
return new ApiError(400, "VALIDATION_ERROR", message);
|
|
}
|
|
|
|
static config(message: string, status = 401): ApiError {
|
|
return new ApiError(status, "CONFIG_ERROR", message);
|
|
}
|
|
|
|
static provider(message: string, status = 500): ApiError {
|
|
if (isAuthRequiredMessage(message)) {
|
|
return ApiError.unauthorized(message);
|
|
}
|
|
return new ApiError(status, "PROVIDER_ERROR", message);
|
|
}
|
|
}
|
|
|
|
export interface ApiErrorPayload {
|
|
success: false;
|
|
requestId?: string;
|
|
code: ApiErrorCode;
|
|
error: string;
|
|
}
|
|
|
|
export function apiErrorResponse(
|
|
error: unknown,
|
|
requestId?: string,
|
|
fallbackMessage = "Request failed"
|
|
): NextResponse<ApiErrorPayload> {
|
|
if (error instanceof ApiError) {
|
|
return NextResponse.json<ApiErrorPayload>(
|
|
{
|
|
success: false,
|
|
requestId,
|
|
code: error.code,
|
|
error: error.message,
|
|
},
|
|
{ status: error.status }
|
|
);
|
|
}
|
|
|
|
const message = error instanceof Error ? error.message : fallbackMessage;
|
|
if (isAuthRequiredMessage(message)) {
|
|
return NextResponse.json<ApiErrorPayload>(
|
|
{
|
|
success: false,
|
|
requestId,
|
|
code: "AUTH_REQUIRED",
|
|
error: message,
|
|
},
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const isRateLimited = message.includes("429");
|
|
|
|
return NextResponse.json<ApiErrorPayload>(
|
|
{
|
|
success: false,
|
|
requestId,
|
|
code: isRateLimited ? "RATE_LIMITED" : "INTERNAL_ERROR",
|
|
error: isRateLimited ? "Rate limit reached. Please wait and try again." : message,
|
|
},
|
|
{ status: isRateLimited ? 429 : 500 }
|
|
);
|
|
}
|
|
|