Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

Cache API drops the fragment, so every chunk I stored was the same entry

Our highest-quality model tier never once worked in production. From the day it shipped, every attempt died with the same line: ERROR_CODE: 7, Failed to load model because protobuf parsing failed I read that as

Our highest-quality model tier never once worked in production. From the day it shipped, every attempt died with the same line:

ERROR_CODE: 7, Failed to load model because protobuf parsing failed

I read that as an out-of-memory symptom and went hunting for memory. The error had already ruled that out and I wasn't listening.

Why memory was a believable story

The tier is genuinely big: 388 MB gzipped, 445 MB after decompression. We had been burned once already by holding the compressed buffer and the decompressed result at the same time, which peaked near 830 MB and killed the tab every time. So when a second failure turned up on the same tier, "too big for the device" fit the shape of what I thought I knew. A previous bug that looked similar makes the new one feel explained before you've looked at it.

What should have stopped me was the clock. Loading and running this thing takes tens of seconds. The failures came back in two or three. A job dying far faster than it could possibly finish is not running out of anything, because it never got started.

What was actually happening

We cache the model in chunks of 32 MB, then write a small manifest last, so that seeing a manifest means the chunks are all there. The keys looked like this:

const chunkKey = (cacheKey, part) => `${cacheKey}#${part}`;
// key#0, key#1, ... key#meta

The Cache API strips the URL fragment. It does it on put and on match, so key#0, key#7 and key#meta are one entry, not three. Every chunk write overwrote the one before it. The manifest goes in last, so what survived was a JSON blob of under thirty bytes.

Then the top of our load path does a plain lookup with no fragment at all:

const cached = await cache.match(cacheKey);

Which hits, because there was only ever one entry. So we handed ONNX Runtime a manifest, and it told us, accurately, that those bytes were not a protobuf.

Every layer here behaved as documented and nothing threw. The writes reported success and the read reported a hit. The parser was the first thing in the whole chain with an opinion about what the bytes actually contained.

The fix, and the gate behind it

Path segments instead of fragments:

const chunkKey = (cacheKey, part) => `${cacheKey}/part/${part}`;

That fixes this bug. It doesn't fix the class of bug, because the cache can still hand you truncated or evicted bytes through no fault of your keys. So there's a second check before anything reaches the parser:

const looksLikeOnnx = (bytes) =>
  bytes.byteLength > 1024 * 1024 && bytes[0] === 0x08;

ONNX is protobuf, and its first field is ir_version, a field-1 varint, so the first byte has to be 0x08. Add a size floor and a tiny manifest can never masquerade as a model again. When the check fails we delete the entry and treat it as a miss, which costs a re-download. I'll take the re-download over feeding questionable bytes to a parser and then reading its error message as gospel about my hardware.

If you cache anything in chunks, it's worth checking your own code for the same shape. Keep # out of cache keys entirely. After writing, read back and count the entries you expect, since overwrites are silent and successful. And validate a magic byte plus a size floor before cached bytes reach a parser.

The part I keep thinking about isn't the fragment rule. That one is in the spec and I could have looked it up any time. It's that I spent weeks treating a symptom as a resource problem when the duration of the failure ruled that out on the first day. A fast failure and a slow failure tell you different things about where the work stopped, and I only knew how to read one of them.

I build koutuxia, a background remover that runs the model in your browser, which is how I met this one.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.