Surface n8n failures + add upload timeout + dedupe MIME/size constants

- Purchase order upload route now returns 502 n8n_rejected when n8n
  responds with a non-2xx status, instead of silently claiming success.
- Add a 10s AbortController timeout on the n8n fetch, returning 504
  n8n_timeout on expiry (502 n8n_unreachable on other network errors).
- Extract ALLOWED_MIME and MAX_FILE_BYTES from upload/route.ts into
  src/lib/purchase-order-schema.ts so the upcoming ingest route can
  share the same validation constants.
This commit is contained in:
DEls 2026-08-19 02:41:46 +02:00
parent 508e78f72b
commit 2be0c8de8b
2 changed files with 34 additions and 12 deletions

View file

@ -1,16 +1,9 @@
import { NextResponse } from "next/server";
import { ALLOWED_MIME, MAX_FILE_BYTES } from "@/lib/purchase-order-schema";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
const ALLOWED_MIME = new Set([
"application/pdf",
"image/jpeg",
"image/png",
"image/webp",
]);
const MAX_FILE_BYTES = 20 * 1024 * 1024;
export async function POST(request: Request) {
const start = Date.now();
@ -55,14 +48,43 @@ export async function POST(request: Request) {
n8nForm.append("size", String(file.size));
let n8nRes: Response;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10_000);
try {
n8nRes = await fetch(n8nUrl, { method: "POST", body: n8nForm });
n8nRes = await fetch(n8nUrl, {
method: "POST",
body: n8nForm,
signal: controller.signal,
});
} catch (err) {
clearTimeout(timeoutId);
const isAbort = err instanceof Error && err.name === "AbortError";
console.error("[po-upload] n8n unreachable", {
filename: file.name,
timed_out: isAbort,
err: String(err),
});
return NextResponse.json({ error: "n8n_unreachable" }, { status: 502 });
return NextResponse.json(
{ error: isAbort ? "n8n_timeout" : "n8n_unreachable" },
{ status: isAbort ? 504 : 502 },
);
}
clearTimeout(timeoutId);
if (!n8nRes.ok) {
console.error("[po-upload] n8n rejected", {
filename: file.name,
n8n_status: n8nRes.status,
statusText: n8nRes.statusText,
});
return NextResponse.json(
{
error: "n8n_rejected",
n8n_status: n8nRes.status,
statusText: n8nRes.statusText,
},
{ status: 502 },
);
}
console.log("[po-upload] forwarded", {

View file

@ -24,13 +24,13 @@ export type PurchaseOrderIngestPayload = {
raw_ocr: Record<string, unknown>;
};
const ALLOWED_MIME = new Set([
export const ALLOWED_MIME = new Set([
"application/pdf",
"image/jpeg",
"image/png",
"image/webp",
]);
const MAX_FILE_BYTES = 20 * 1024 * 1024;
export const MAX_FILE_BYTES = 20 * 1024 * 1024;
export type ValidationResult =
| { ok: true; payload: PurchaseOrderIngestPayload }