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.
This commit is contained in:
Marc-André Bombeck committed 2026-07-08 21:56:17 +02:00
1 parent 0b973f4f76
commit eed705be7b
3 files changed
+32 -11

No files matched your search

+4 -3
View File
@@ -365,10 +365,11 @@ async function postUpload(request: Request): Promise<Response> {
// 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(
@@ -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", () => {
+19 -8
View File
@@ -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 };
}
}