- 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.
102 lines
No EOL
2.8 KiB
TypeScript
102 lines
No EOL
2.8 KiB
TypeScript
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";
|
|
|
|
export async function POST(request: Request) {
|
|
const start = Date.now();
|
|
|
|
let form: FormData;
|
|
try {
|
|
form = await request.formData();
|
|
} catch {
|
|
return NextResponse.json({ error: "invalid_form_data" }, { status: 400 });
|
|
}
|
|
|
|
const file = form.get("file");
|
|
if (!(file instanceof File)) {
|
|
return NextResponse.json({ error: "missing_file" }, { status: 400 });
|
|
}
|
|
if (file.size === 0) {
|
|
return NextResponse.json({ error: "empty_file" }, { status: 400 });
|
|
}
|
|
if (file.size > MAX_FILE_BYTES) {
|
|
return NextResponse.json(
|
|
{ error: "file_too_large", max_bytes: MAX_FILE_BYTES },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
if (!ALLOWED_MIME.has(file.type)) {
|
|
return NextResponse.json(
|
|
{ error: "unsupported_type", got: file.type, allowed: [...ALLOWED_MIME] },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const n8nUrl = process.env.N8N_PO_INGEST_URL;
|
|
if (!n8nUrl) {
|
|
console.error("[po-upload] N8N_PO_INGEST_URL not configured");
|
|
return NextResponse.json({ error: "n8n_unconfigured" }, { status: 500 });
|
|
}
|
|
|
|
// Forward to n8n as multipart
|
|
const n8nForm = new FormData();
|
|
n8nForm.append("file", file, file.name);
|
|
n8nForm.append("filename", file.name);
|
|
n8nForm.append("contentType", file.type);
|
|
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,
|
|
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: 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", {
|
|
filename: file.name,
|
|
size: file.size,
|
|
contentType: file.type,
|
|
n8n_status: n8nRes.status,
|
|
duration_ms: Date.now() - start,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{ status: "processing", filename: file.name },
|
|
{ status: 202 },
|
|
);
|
|
} |