fix(documents): wait out the upload route's busy answer instead of failing

The inbound route admits two uploads per person (four per process) and
answers the next with 429 `uploadBusy` and `Retry-After: 1`. The vault
queue sent three at once, so a multi-file drop provoked that answer
itself, and the card read it as the hourly limit: "Try again in a few
minutes".

The queue now runs at `DOCUMENT_UPLOAD_PER_USER_CONCURRENCY`, moved to
`inbound-constants.ts` so client and route share one number. A busy
answer is classified on its own, carries its Retry-After, and is retried
for the same file (Retry-After clamped to 1-10 s plus jitter, at most six
attempts within 30 s) while the row stays in its uploading state. Only a
spent budget shows an error, with copy that says the server was busy.
Unmounting aborts any pending wait. The Coach's new-file attach uses the
same retry. The hourly-limit copy now names the hour.
This commit is contained in:
Marc-André Bombeck committed 2026-11-06 12:35:25 +01:00
1 parent 53e5b47e0d
commit 2e14b2d8b5
27 files changed
+633 -44

No files matched your search

@@ -159,8 +159,8 @@ import { getAiCapability } from "@/lib/ai/capabilities/gate";
import {
acquireDocumentUploadSlot,
DOCUMENT_UPLOAD_GLOBAL_CONCURRENCY,
DOCUMENT_UPLOAD_PER_USER_CONCURRENCY,
} from "@/lib/documents/upload-policy";
import { DOCUMENT_UPLOAD_PER_USER_CONCURRENCY } from "@/lib/documents/inbound-constants";
const post = POST as unknown as (r: Request) => Promise<Response>;
const get = GET as unknown as (r: Request) => Promise<Response>;
@@ -413,6 +413,8 @@ describe("POST /api/documents/inbound (vault upload)", () => {
try {
const res = await post(request);
expect(res.status).toBe(429);
// The web upload queue waits this long and retries the same file.
expect(res.headers.get("Retry-After")).toBe("1");
await expect(res.json()).resolves.toMatchObject({
meta: { reason: "uploadBusy" },
});
@@ -431,6 +433,8 @@ describe("POST /api/documents/inbound (vault upload)", () => {
try {
const res = await post(mkUpload());
expect(res.status).toBe(429);
// The web upload queue waits this long and retries the same file.
expect(res.headers.get("Retry-After")).toBe("1");
await expect(res.json()).resolves.toMatchObject({
meta: { reason: "uploadBusy" },
});