Files

73 lines
2.3 KiB
TypeScript

import { DomainError } from "@/db/repository";
function requireJsonMutation(request: Request): void {
const contentType = request.headers
.get("content-type")
?.split(";", 1)[0]
.trim()
.toLowerCase();
if (contentType !== "application/json") {
throw new DomainError(
"json_required",
"写操作只接受 application/json",
415,
);
}
const contentLength = Number(request.headers.get("content-length") ?? "0");
if (Number.isFinite(contentLength) && contentLength > 32_768) {
throw new DomainError("request_too_large", "请求内容过大", 413);
}
const origin = request.headers.get("origin");
if (origin && origin !== new URL(request.url).origin) {
throw new DomainError("invalid_origin", "拒绝跨站写操作", 403);
}
const fetchSite = request.headers.get("sec-fetch-site");
if (fetchSite && fetchSite !== "same-origin") {
throw new DomainError("invalid_fetch_site", "拒绝跨站写操作", 403);
}
}
export async function readJsonMutation<T>(request: Request): Promise<T> {
requireJsonMutation(request);
const body = await request.text();
if (new TextEncoder().encode(body).byteLength > 32_768) {
throw new DomainError("request_too_large", "请求内容过大", 413);
}
try {
return JSON.parse(body) as T;
} catch {
throw new DomainError("invalid_json", "请求不是有效 JSON", 400);
}
}
export function apiError(error: unknown) {
if (error instanceof DomainError) {
const headers = new Headers({ "cache-control": "no-store" });
if (error.retryAfterSeconds !== undefined) {
headers.set("retry-after", String(error.retryAfterSeconds));
}
return Response.json({
error_code: error.code,
error: error.code,
message: error.message,
retryable: error.retryable,
...(error.retryAfterSeconds === undefined
? {}
: { retry_after_seconds: error.retryAfterSeconds }),
...(error.actionUrl === undefined ? {} : { action_url: error.actionUrl }),
}, { status: error.status, headers });
}
console.error("Unhandled NekoNest Cloud API error", error);
return Response.json(
{
error_code: "internal_error",
error: "internal_error",
message: "服务暂时不可用,请稍后重试",
retryable: true,
},
{ status: 500, headers: { "cache-control": "no-store" } },
);
}