From eed705be7bac87d48bed90c4d4323f5e4112a683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 8 Jul 2026 21:56:17 +0200 Subject: [PATCH] fix(documents): make auto-index enqueue never fail a stored upload The upload route awaited an unguarded enqueueDocumentIndex after the document was already committed. A transient boss.send rejection (a DB hiccup) would then surface as a 500 for an upload that in fact stored the file. Guard the send inside the enqueue (a failure annotates and no-ops, matching the missing-boss path) and drop the await at the call site so the response never blocks on or fails because of background indexing. --- src/app/api/documents/inbound/route.ts | 7 ++--- src/lib/jobs/__tests__/document-index.test.ts | 9 +++++++ src/lib/jobs/document-index.ts | 27 +++++++++++++------ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/app/api/documents/inbound/route.ts b/src/app/api/documents/inbound/route.ts index e3985b0f7..63cd5cc71 100644 --- a/src/app/api/documents/inbound/route.ts +++ b/src/app/api/documents/inbound/route.ts @@ -365,10 +365,11 @@ async function postUpload(request: Request): Promise { // Auto-index the freshly stored document for content search: enqueue a // fire-and-forget background job (provider-first, local text-layer fallback). - // The upload response never blocks on or fails because of indexing; a missing - // boss (worker not up) is a silent no-op. Only fresh inserts enqueue — a + // The upload response never blocks on or fails because of indexing — the + // enqueue is not awaited and swallows its own errors (a missing boss or a + // transient send failure is a silent no-op). Only fresh inserts enqueue — a // duplicate upload returns early above and never reaches here. - await enqueueDocumentIndex(user.id, document.id); + void enqueueDocumentIndex(user.id, document.id); const links = await loadConditionLinks(user.id, [document.id]); return apiSuccess( diff --git a/src/lib/jobs/__tests__/document-index.test.ts b/src/lib/jobs/__tests__/document-index.test.ts index 3de576f36..e175bc1a8 100644 --- a/src/lib/jobs/__tests__/document-index.test.ts +++ b/src/lib/jobs/__tests__/document-index.test.ts @@ -43,6 +43,15 @@ describe("enqueueDocumentIndex", () => { const result = await enqueueDocumentIndex("user-1", "doc-1"); expect(result).toEqual({ enqueued: false }); }); + + it("swallows a boss.send failure to a no-op (never fails the upload)", async () => { + const send = vi.fn().mockRejectedValue(new Error("db down")); + vi.mocked(getGlobalBoss).mockReturnValue({ send } as never); + // Must resolve, never reject — the upload has already committed. + await expect(enqueueDocumentIndex("user-1", "doc-1")).resolves.toEqual({ + enqueued: false, + }); + }); }); describe("runDocumentIndex", () => { diff --git a/src/lib/jobs/document-index.ts b/src/lib/jobs/document-index.ts index bc4396759..88f042ff7 100644 --- a/src/lib/jobs/document-index.ts +++ b/src/lib/jobs/document-index.ts @@ -49,7 +49,10 @@ export async function runDocumentIndex( /** * Enqueue a per-document index job. Coalesced by `singletonKey` per document so * a duplicate/idempotent re-upload cannot pile up parallel runs. Fire-and- - * forget — a missing boss (worker not up) is a no-op, never an upload failure. + * forget — a missing boss (worker not up) is a no-op, and a `boss.send` failure + * (a transient DB hiccup) is swallowed to a no-op too, so enqueue can NEVER fail + * an already-stored upload. Indexing is a background nicety; the stored document + * is the contract. A dropped enqueue is recoverable via the corpus backfill. */ export async function enqueueDocumentIndex( userId: string, @@ -62,11 +65,19 @@ export async function enqueueDocumentIndex( documentId, enqueuedAt: new Date().toISOString(), }; - const jobId = await boss.send(DOCUMENT_INDEX_QUEUE, payload, { - retryLimit: 2, - retryDelay: 30, - retryBackoff: true, - singletonKey: `document-index|${documentId}`, - }); - return { enqueued: Boolean(jobId) }; + try { + const jobId = await boss.send(DOCUMENT_INDEX_QUEUE, payload, { + retryLimit: 2, + retryDelay: 30, + retryBackoff: true, + singletonKey: `document-index|${documentId}`, + }); + return { enqueued: Boolean(jobId) }; + } catch (err) { + annotate({ + action: { name: "documents.autoIndex.enqueueFailed" }, + meta: { documentId, reason: err instanceof Error ? err.name : "unknown" }, + }); + return { enqueued: false }; + } }