Skip to content

projector(subjects): GET /subjects/<malformed percent-encoding> throws a raw URIError out of the handler — 500 instead of 400/404 #2693

Closed

Description

@dezren39

Problem

The projector's joined-subject read endpoint throws on a malformed percent-encoding instead of answering with a clean error (main b3f6d2b):

pm-stack/projector/src/server.ts:175-176

if (path.startsWith("/subjects/") && method === "GET") {
  const subject = decodeURIComponent(path.slice("/subjects/".length));

decodeURIComponent is unguarded, and this handler has no try/catch (unlike /health and the webhook path in the same file). A subject containing an incomplete UTF-8 percent sequence (%E0%A4%A) or a non-hex escape (%zz) raises a raw URIError that escapes handleRequest entirely — under Bun.serve the request fails as an unhandled 500 with no JSON error body, instead of the endpoint's own 404 {"error":"subject_not_found"} or a 400 naming the bad encoding.

Subjects are external keys (kaneo:<projectSlug>/<taskId>, grist:<docId>/<tableId>/<rowId>, gh:<ref> — schema.sql), i.e. caller-supplied strings interpolated into a URL path by the aggregator and other clients, so a malformed or hand-built link reaches this code as-is.

Evidence (verified this run)

Probe on the laptop (~/git/fleet/probe-2026-10-30-laneB-projector/probe.ts, real handleRequest + migrate against an in-memory libSQL DB, main b3f6d2b):

GET /subjects/%E0%A4%A     -> THREW URIError: URI error
GET /subjects/%zz          -> THREW URIError: URI error
GET /subjects/valid%20name -> status 404 body {"error":"subject_not_found"}
GET /subjects/plain        -> status 404 body {"error":"subject_not_found"}

Well-formed encodings decode and 404 cleanly; only the malformed forms escape the handler as a thrown exception.

Why it matters

This is the projector's one joined read (links + item state + unified activity in one call — aggregator requirement #1 + #2). Any client that builds the path from an untrusted or hand-edited subject gets an opaque server error with no JSON body to handle, and the throw lands in the server log as an unhandled exception rather than a client error. Every other structured input on this server is validated at the boundary (webhook HMAC + bounded body, link ref regex); this decode is the loose one.

Acceptance criteria

  • GET /subjects/<malformed percent-encoding> (e.g. %E0%A4%A, %zz, a trailing %) returns a JSON 400 (or the endpoint's 404) — handleRequest never throws for any path input.
  • Well-formed subjects are unchanged: an existing subject still returns 200 with its view, an unknown one still returns 404 {"error":"subject_not_found"}.
  • A handler-level test pins the malformed forms alongside the well-formed control cases.

Distinct from #2633 (the same defect family in lab-ops-hub's file route — different app, different handler) and from #1547 (projector read-API limit validation; its acceptance criteria cover only the limit parameter).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    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/pm-stackpm-stack (Pad/Grist) in developing-today/code
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — design-architect pass 20 (2026-10-30): this issue is ADOPTED as Phase 3 of design #2747 (one decode for path segments — a malformed percent-encoding is a 400, never a 500) and linked as a sub-issue of #2747 (verified in its sub_issues list); labelled source/design-architect. Do NOT re-file, re-scope, or split it — record the phase structure in triage instead. One decision is recorded on #2747 that sharpens this issue's acceptance: where it says "400 (or the endpoint's 404)", the design settles 400 uniformly (malformed spelling = client syntax error; a well-formed miss keeps the existing 404 {"error":"subject_not_found"}), with a JSON body and no raw URIError text. Phase 3 also carries the family's closing audit (gated on Phases 1–2 landing, #2748 + #2633/#2720): a grep over pm-stack/ + pkgs/ proving every server-side path-segment decodeURIComponent goes through a guard, recorded on #2747. The shared probe matrix is on #2747 and in the design doc files/designs/code-path-segment-decode.md. This issue keeps its own already-queued triage packet — no new packet was created for it.

  4. 14 remaining items

  5. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code), Lane A shift
    • Session / ID: queue task 4099 (repo-health-code-pilot), completed 2026-10-31T02:14Z
    • Found via: queue claim (park resume)
    • Task: PR 2906 merged — /subjects/ decode guard returns JSON 400 on malformed percent-encoding; Codex review completed on head 1ad8da6 with no findings; mergeable clean; no PR CI gates pm-stack/projector (laptop suite 27/27). Merge commit 0281d10. This issue auto-closed by the merge.
    • Expectation: done — task 4099 completed as fixed_merged; in-flight labels removed.
    • Links: PR fix(projector): malformed percent-encoding in /subjects/ returns JSON 400 (#2693) #2906 (branch queue/4099-2693-subjects-decode)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/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