Repository navigation
Description
Bug: lab-ops-hub file-upload R2 key is Date.now() + sanitized filename — same-millisecond uploads collide, overwrite the object, and 500 on the UNIQUE key
POST /api/files in pkgs/lab-ops-hub/src/index.ts derives the R2 key from the wall-clock millisecond and the sanitized filename only:
const key = `uploads/${Date.now()}-${file.name.replace(/[^\w.\-]+/g, "_")}`;
await env.FILES.put(key, buf, { ... });
const r = await env.DB.prepare("INSERT INTO files (r2_key, filename, content_type, size) VALUES (?1,?2,?3,?4)")
.bind(key, file.name, file.type || null, buf.byteLength)
.run();
await env.JOBS.send({ type: "index-file", r2Key: key });But pkgs/lab-ops-hub/schema.sql declares files.r2_key TEXT NOT NULL UNIQUE. Two uploads that resolve to the same key therefore corrupt state instead of both succeeding:
- Same filename, same millisecond (a scripted double-submit, a retried upload, a test loop): the keys are byte-identical. The second
FILES.putsilently overwrites the first file's R2 object, then its D1INSERTthrowsUNIQUE constraint failed: files.r2_keyand the request 500s via the outer catch. The surviving D1 row still describes the first upload (filename/size), whileGET /api/files/<key>now serves the second upload's bytes — and the first upload's queuedindex-filejob hashes those wrong bytes and stamps that sha256 +indexed=1onto the first row. - Different filenames that sanitize to the same string (e.g.
my file.pdfvsmy:file.pdf→ bothmy_file.pdf) collide the same way in the same millisecond.
Verified this run (main 5a34fa70)
Sandbox Bun probe using the verbatim key expression plus a bun:sqlite table built from the real schema's files definition, replaying the handler's put → insert order:
keyFor("report.pdf", t)called twice at the samet→ identical keys (uploads/1761782400000-report.pdf).keyFor("my file.pdf", t) === keyFor("my:file.pdf", t)→ identical keys.- Upload A (
report.pdf, bodyORIGINAL-A) → 201; upload B (same name, same ms, bodyREPLACEMENT-B) →INSERT FAILED (SQLiteError: UNIQUE constraint failed: files.r2_key); the R2 map for the key then holdsREPLACEMENT-Bwhile the only D1 row still records size 10 (A's size).
Probe: hidden_files/probe-2026-10-29-laneB-uploadkey/probe.ts in the keep-the-four-repos-healthy goal workspace.
Why it matters
The files table is the hub's record of what was uploaded; a collision silently swaps the bytes behind an existing record (the row's size/content-type/sha256 no longer describe what a download returns) and turns a legitimate second upload into an opaque 500. Millisecond collisions are not exotic for automated callers — retries and double-submits are exactly the traffic that hits an upload endpoint twice in the same millisecond.
Acceptance criteria
- Two uploads of the same filename in the same millisecond both succeed (201) with distinct R2 keys and two correct D1 rows (e.g. a random/UUID or row-id component in the key), and each download returns its own bytes.
- Filenames that sanitize to the same string no longer collide.
- A regression test exercises back-to-back same-name uploads (the key derivation should live somewhere testable, per the lab-ops-hub: the #500 crawl-timeout fix has no regression test — crawlFallback's fetcher is inline in the untestable Worker entry #524 pattern of moving logic out of the Worker entry into
src/lib.ts). - Existing behavior otherwise unchanged: key prefix
uploads/, filename sanitization, queueindex-filemessage per successful upload.
Dedup
gh searches (open + closed) for r2_key, uploads/, UNIQUE constraint, and lab-ops-hub file/upload terms return only pkgs/id upload issues (#1549, #2363 — different codebase, filename-keyed store) and #441 (lab-ops auth, closed). No issue covers the lab-ops-hub upload key derivation.
— filed by new-issue-creator-code-b (Lane B), 2026-10-29 run
Activity
developing-today commentedon Oct 30, 2026
CODE-1266
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
8 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
uploadKey(name, now, rand)insrc/lib.ts(random suffix, injectable rand), handler uses it, regression tests intest/lib.test.ts.Merge pull request #3440 from developing-today/queue/5345-2478-upload…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done
uploadKey(name, now, rand)insrc/lib.tsreturnsuploads/<now>-<rand>-<sanitized>; the /api/files handler passescrypto.randomUUID(). Same-millisecond and sanitize-equivalent uploads can no longer collide on the UNIQUEr2_keyafter overwriting the R2 object.npm run test:ts51/51 pass including 4 new uploadKey tests (distinct rand → distinct keys for same name+ms and for themy file.pdf/my:file.pdfpair; determinism + exact shape; default UUID rand never repeats). Codex review on 2c980b3 completed with no findings. tsc not run locally (fresh worktree, no node_modules) — noted on the PR.