Repository navigation
Description
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) —handleRequestnever 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
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 30, 2026
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 existing404 {"error":"subject_not_found"}), with a JSON body and no rawURIErrortext. Phase 3 also carries the family's closing audit (gated on Phases 1–2 landing, #2748 + #2633/#2720): a grep overpm-stack/+pkgs/proving every server-side path-segmentdecodeURIComponentgoes through a guard, recorded on #2747. The shared probe matrix is on #2747 and in the design docfiles/designs/code-path-segment-decode.md. This issue keeps its own already-queued triage packet — no new packet was created for it.14 remaining items
dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
fix(projector): malformed percent-encoding in /subjects/ returns JSON…
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done