This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.
Here's the thing about the bug I'm about to describe: for the first hour, I was completely sure I knew where it lived. I was wrong, and I want to walk you through exactly how wrong, because the actual location was almost insulting in how little sense it made until suddenly it made total sense.
The Setup
npmx.dev is an open-source browser for the npm registry. One of its features lets you compare two packages side by side — versions, install size, dependencies, the works. Somebody filed a bug: compare tinyclip against anything, and it would report tinyclip stuck at version 0.0.1 — its very first-ever published version — when the real latest was 0.1.8.
My first thought was the obvious one: the comparison logic is reading the wrong field. Maybe it's grabbing the first published version instead of the latest dist-tag. Confident, reasonable, and completely wrong.
Chasing the Wrong Suspect
I went into the comparison composable expecting to find some sloppy array indexing or a fallback that defaulted to the wrong thing. Instead I found totally reasonable code: fetch the package's registry data, read dist-tags.latest, done. No obvious bug. No off-by-one. Nothing.
This is the part of debugging nobody puts in the tutorial: the moment where your working theory just quietly stops being true, and you have to admit you don't know where the bug is anymore.
So I went one level down, into the actual fetch function behind that call. And that's where things got strange.
The Parameter That Wasn't Doing Anything
Buried in the fetch wrapper was this:
_ttl: number = FETCH_CACHE_DEFAULT_TTL,
An underscore-prefixed parameter. If you've spent any time in TypeScript, you know what that prefix means: "accepted, but deliberately unused." Someone had designed a whole caching contract around a configurable time-to-live — and on the client, it was accepted into the function signature and then never touched again.
Right next to it, the actual caching behavior was hardcoded:
cache: 'force-cache',
I want to be honest about my reaction to this, because it wasn't "aha, found it" right away. It was closer to "okay but that can't be the whole thing, this is a comparison page, not a caching page." I almost moved on. The bug I was hunting was about wrong data, not stale data — or so I thought, before I remembered that from a browser's perspective, those are frequently the exact same problem wearing different clothes.
Why force-cache Is a Trap
force-cache tells the browser one very specific thing: if any cached response already exists for this URL, use it — don't bother checking whether it's still fresh. That's different from the browser's normal behavior, which checks Cache-Control headers and quietly revalidates once something goes stale.
So here's what was actually happening: the very first time your browser ever fetched tinyclip's registry data — however long ago that was — it locked that response in. New versions could publish on npm every week after that, and your browser would never know, because it had been explicitly told not to ask again.
It wasn't a data bug. It was a bug about when the browser was allowed to ask for new data, disguised as a data bug.
Making Sure I Wasn't Just Telling Myself a Nice Story
Before I trusted this theory, I checked what registry.npmjs.org actually sends back on a real request:
cache-control: public, max-age=300
etag: "..."
The registry was already doing its job — five-minute freshness window, an ETag for cheap revalidation after that. The app just wasn't listening to any of it. That was the moment the theory stopped being a theory.
The Fix
- cache: 'force-cache',
+ cache: 'default',
One line, in two places (there's a client branch and an identical server-fallback branch — both needed the same fix, or the bug would've just moved house). 'default' means: respect what the server actually told you about freshness, instead of assuming a months-old response is still gospel.
I ran the full test suite afterward — 1,000+ tests, all green — and checked the network panel to confirm requests were genuinely round-tripping instead of resolving instantly from a frozen local copy.
PR, if you want to see the actual diff: npmx-dev/npmx.dev#3156
What I'd Actually Tell Someone About This
If a bug report describes wrong-looking data, "check the data logic" is a completely reasonable first move — it's just not the only move. Data can be wrong because the logic is wrong, or because the logic is right and it's being fed a version of reality that's frozen in time. Those look identical from the outside. They are not identical to fix.
And the real lesson, the one I keep coming back to: an unused parameter isn't nothing. It's a question the codebase is quietly still asking, that nobody's gotten around to answering. _ttl sat there the whole time, patiently doing nothing, waiting for someone to notice that it should have been doing something.
I noticed. Eventually.
Top comments (0)