// app/api/tasks/[id]/fail/route.ts // POST /api/tasks/[id]/fail // Atomic state transition: in_progress → failed. Body: { error, log, retry }. import { NextRequest, NextResponse } from "next/server"; import { BASE_URL, fetchRecord, updateRecord, PBRecord } from "@/lib/pb"; export const dynamic = "force-dynamic"; const COLLECTION = "tasks"; const MAX_RETRY = 3; export async function POST(req: NextRequest, { params }: { params: { id: string } }) { let body: any = {}; try { body = await req.json(); } catch { return NextResponse.json({ error: "invalid JSON body" }, { status: 400 }); } const errorMsg = (body?.error ?? "unknown error").toString().slice(0, 5000); const log = body?.log ?? null; // Atomic transition: in_progress → failed. const url = `${BASE_URL}/api/collections/${COLLECTION}/records/${encodeURIComponent(params.id)}?filter=status="in_progress"`; const res = await fetch(url, { method: "PATCH", headers: { Authorization: process.env.PB_TOKEN as string, "Content-Type": "application/json", }, body: JSON.stringify({ status: "failed", error: errorMsg, log, completed_at: new Date().toISOString().replace("T", " ").replace("Z", ""), }), cache: "no-store", }); if (res.status === 404) { return NextResponse.json( { ok: false, error: "task not found or not in progress" }, { status: 409 }, ); } if (!res.ok) { const text = await res.text(); return NextResponse.json({ ok: false, error: `PB ${res.status}: ${text.slice(0, 200)}` }, { status: 500 }); } const failed: PBRecord = await res.json(); // Auto-retry if under the cap and the caller asked for it. let requeued = false; if (body?.retry !== false) { const current = await fetchRecord(COLLECTION, params.id); const retryCount = (current.retry_count as number) ?? 0; if (retryCount < MAX_RETRY) { await updateRecord(COLLECTION, params.id, { status: "ready", retry_count: retryCount + 1, error: null, completed_at: null, claimed_by: null, claimed_at: null, started_at: null, }); requeued = true; } } return NextResponse.json({ ok: true, task: failed, requeued }); }