Build1 distinct publisher3 min readUpdated
A trailing hyphen in a crawled URL turned single-item lookups into full-dataset reads, and the edge logs filed every one of them as outcome: ok.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
The operator of AI Change Watch, an independent crawler that records what 15 AI vendors publish about their own models, found that `GET /v1/events/{id}` with an empty id does not return 404 [1][2]. It resolves to `GET /v1/events/`, which is the list endpoint, and the API answers 200 with `{ data: [ ...every recent event... ] }` [2][7]. That matters because the id in that path is a string assembled by hand from a URL, and truncated URLs arrive constantly: crawlers generate them, and so do chat clients and mail readers that break a long link across lines and leave the tail behind [6].
The mechanics are dull, which is the point. Every change lives at a URL like `/event/openai-gpt-4-deprecated-bf_20260723143458_0003`, slug for humans, id after the last hyphen [3]. The parser does what you would write: `lastIndexOf('-')`, then `slice(i + 1)`, falling back to the whole param if there is no hyphen [4]. Given `/event/google-`, `lastIndexOf` finds the final character, the slice returns the empty string, and the fetch goes out as `/v1/events/` [5].
On this site the collection payload was then handed to a mapper that read `.title`, `.providerName` and `.severity` off an array [8]. All undefined, no error, because reading a missing property off an array is legal [8]. The page rendered an event-shaped object with every field empty until it reached `providerName.charAt(0)` and threw [9]. So `/event/google-`, `/event/aws-` and `/ja/event/groq-groq-` returned 500 where a 404 was owed, live, for as long as those URLs had existed [10]. Note the sequencing: the array had to arrive and be mapped before the crash, so the bulk response crossed the wire on every one of those requests [1]. The visible symptom was a broken page; the invisible one was that each truncated URL bought a full recent-events read instead of a single record, and anything enumerating slug variants multiplies that one-for-one [2].
The logging made it worse. The bug was found by querying Cloudflare's observability API for 5xx responses, and the obvious filter field was useless: `$workers.outcome = "ok"` for every single one [11]. A rendered 500 is a successful worker invocation, so the signal lives in `$metadata.error`, and edge logs filtered by outcome simply do not contain application-level 500s [12]. A sibling route then hid the pattern by accident. `/pricing/history/<slug>-<id>` never 500'd, not because it was more careful, but because it filters on `type === 'pricing_changed'`; the junk object had `type: undefined`, the filter rejected it, and the page 404'd correctly [13]. That made the defect look route-specific, and time went into reading the event page, the one place it was not [14].
The general shape: a REST detail path with a missing key silently degrades into the collection path, and `/things/{id}` and `/things/` are different endpoints with different response shapes separated only by a string you built [15]. No status code marks the difference. Both are 200, both return `{ data: ... }`, and the only distinction is object versus array, which JavaScript will let you read properties off either way [16].
The fix went into the data layer, not the page: reject an empty id before the request is sent, and only treat a DTO that actually carries an `id` as an event, so a list payload or `{data:null}` becomes not-found rather than a half-populated object [17]. Worth watching in your own stack is the third condition in that helper, where a null response means the API was never reached at all, at build time or in local dev with no base URL, as distinct from the API answering that nothing exists [18]. Collapse those two and a build-time render turns every page into a 404 [18]. And check which field your edge log filter actually uses, because outcome will tell you everything is fine [12].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
AI Change Watch is a small independent project that crawls what 15 AI vendors publish about their own models (deprecation tables, lifecycle pages, pricing and SDK releases) and records every change.
GET /v1/events/{id} with an empty id becomes GET /v1/events/, which is not a 404 but the list endpoint, and it returns 200 OK.
Every change on the site lives at a URL like /event/openai-gpt-4-deprecated-bf_20260723143458_0003: slug for humans, id after the last hyphen for the lookup.
The parser eventIdFromParam takes the index of the last hyphen and returns param.slice(i + 1), or the whole param if there is no hyphen.
For /event/google-, lastIndexOf('-') finds the final character, slice(i + 1) returns the empty string, and the fetch goes out as /v1/events/.
Crawlers generate trailing-hyphen URLs, as do chat clients and mail readers that break a long URL across lines and leave the tail behind.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Self-reported but mechanically specific
The causal chain is unusually well specified for a single-source post: the parser, the mapper line, the throwing expression, the named 500-serving URLs, the exact log field ($workers.outcome versus $metadata.error) and the shipped fix are all shown verbatim, and each step is independently checkable by a reader against their own stack. What is missing is any external corroboration: everything rests on one first-person account by the project's own author, with no captured log samples, response bodies or third-party confirmation.
One self-deployed fix, no external uptake
The only observable adoption is the author shipping the guards on their own site, with four previously-500 URLs reported as 404 afterwards. There is no evidence of any other codebase adopting the pattern, no indication other APIs were audited with the suggested curl, and no measurement of how often truncated URLs actually arrive.
Mechanism solid, generality asserted
Slightly overstated relative to evidence. The demonstrated facts are narrow and well supported within one codebase, but the framing — a bug 'a lot of REST clients have and nobody notices' — extends beyond what one project can show, and the cost angle (collection reads replacing single-record reads) is presented as mechanism without any payload size or request volume behind it. The observability point is if anything understated, since it is the finding least likely to be known.
Own-project promotion, no vendor conflict
The post is published under the project's own dev.to account and opens by naming AI Change Watch and what it tracks, so the write-up doubles as distribution for the author's product. Against that, the content is a self-critical post-mortem of the author's own defect, recommends no paid product, and the one vendor named (Cloudflare) is criticized in effect rather than promoted — including the suggestion that API owners reject a trailing slash outright, which the author admits not doing.
Moderate: checkable detail, single voice
Confidence is held down by the single-publisher, single-author basis and by the absence of independent verification of either the pre-fix 500s or the log-field behaviour. It is held up by how specific and reader-verifiable the claims are — code, field names, URLs and a curl any reader can run — which limits room for misreporting of the mechanism itself.
build
A build step instead of a backend: 1,025 records, 8 locales, no runtime API1 distinct publisher
build
Allow-list the closed set, block-list the open one: 193 thin geo pages, one gate1 distinct publisher
build
The third answer: a dead-code tool allowed to say "not traced yet"1 distinct publisher
build
A $5-a-month monitoring SaaS on Workers, Turso and R2 is a cost datapoint, not a blueprint1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 18, 2026