diff --git a/src/app/api/purchase-orders/upload/route.ts b/src/app/api/purchase-orders/upload/route.ts index cb16a63..24bc2f6 100644 --- a/src/app/api/purchase-orders/upload/route.ts +++ b/src/app/api/purchase-orders/upload/route.ts @@ -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", { diff --git a/src/lib/purchase-order-schema.ts b/src/lib/purchase-order-schema.ts index cfed077..4a066c7 100644 --- a/src/lib/purchase-order-schema.ts +++ b/src/lib/purchase-order-schema.ts @@ -24,13 +24,13 @@ export type PurchaseOrderIngestPayload = { raw_ocr: Record; }; -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 }