Skip to content

lab-ops-hub: file-upload R2 key is Date.now()+filename — same-millisecond uploads collide, overwrite the object, then 500 on the UNIQUE r2_key #2478

Closed

Description

@dezren39

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.put silently overwrites the first file's R2 object, then its D1 INSERT throws UNIQUE constraint failed: files.r2_key and the request 500s via the outer catch. The surviving D1 row still describes the first upload (filename/size), while GET /api/files/<key> now serves the second upload's bytes — and the first upload's queued index-file job hashes those wrong bytes and stamps that sha256 + indexed=1 onto the first row.
  • Different filenames that sanitize to the same string (e.g. my file.pdf vs my:file.pdf → both my_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 same t → identical keys (uploads/1761782400000-report.pdf).
  • keyFor("my file.pdf", t) === keyFor("my:file.pdf", t) → identical keys.
  • Upload A (report.pdf, body ORIGINAL-A) → 201; upload B (same name, same ms, body REPLACEMENT-B) → INSERT FAILED (SQLiteError: UNIQUE constraint failed: files.r2_key); the R2 map for the key then holds REPLACEMENT-B while 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, queue index-file message 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

  1. developing-today commented on Oct 30, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/lab-ops-hubPackage/app: lab-ops-hub (pkgs/ or apps/ in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (Lane B, slug new-issue-creator-code-b)
    • Session / ID: cron job new-issue-creator-code-b, run 2026-10-30T00:30:43Z
    • Found via: filed this run
    • Task: Filed after verifying the collision with a sandbox Bun probe (verbatim key expression from src/index.ts + bun:sqlite replay of the schema's UNIQUE r2_key in the handler's put→insert order): same-ms same-name uploads produce identical keys, the second overwrites the R2 object and 500s on the INSERT, leaving the surviving row describing the wrong bytes. Triage packet enqueued to repo-health-code-pilot.
    • Expectation: effort/xs — triage pins the uniquifier shape (random suffix vs row-id-derived key) and the testable home for key derivation, then routes a small_fix packet.
    • Links: packet packet-2478-triage.json (queue repo-health-code-pilot); evidence issue-code-labops-upload-key-body.md + probe-2026-10-29-laneB-uploadkey/ in the goal's hidden_files
  4. added
    priority/lowNice to have; do when convenient
    and removed
    priority/lowNice to have; do when convenient
    on Oct 30, 2026
  5. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  6. 8 remaining items

  7. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. added a commit that references this issue on Nov 1, 2026
  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code), lane F shift
    • Session / ID: queue task 5345
    • Found via: queue claim
    • Task: Fixed — uploadKey(name, now, rand) in src/lib.ts returns uploads/<now>-<rand>-<sanitized>; the /api/files handler passes crypto.randomUUID(). Same-millisecond and sanitize-equivalent uploads can no longer collide on the UNIQUE r2_key after overwriting the R2 object.
    • Expectation: met — PR lab-ops-hub: uniquify upload R2 keys with a random segment (#2478) #3440 merged (merge commit 98a4eb8).
    • Verification: laptop npm run test:ts 51/51 pass including 4 new uploadKey tests (distinct rand → distinct keys for same name+ms and for the my file.pdf/my:file.pdf pair; 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.
    • Links: PR lab-ops-hub: uniquify upload R2 keys with a random segment (#2478) #3440
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/lab-ops-hubPackage/app: lab-ops-hub (pkgs/ or apps/ in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions