A 39-Byte STL File, and the Four Bugs Hiding Behind It

Someone on the Bambu Lab forum told us our STL repair tool had handed him a 39 byte file with no geometry in it, after reporting that the repair had succeeded. He was right. The cause had nothing to do with the repair.

What {"error":"Captcha verification failed"} was doing inside an STL file

The file he received was this, in full: {"error":"Captcha verification failed"}

Count it. That string is exactly 39 bytes. His browser had asked for an STL download, the server answered 403 with a JSON body, and the browser saved that body under the filename it was expecting. He opened it in a slicer and got nothing, which is the right behaviour for a file containing an error message.

Why a download sat behind a captcha at all

Our download route is /api/convert/download/{jobId}. It matched the pattern /api/convert/** in PROTECTED_PATTERNS, so it inherited the captcha gate we had put on job submission. That gate checks a guest_session_valid cookie whose TTL is 600 seconds.

Conversions can run to a watchdog limit of 1200 seconds.

Those two numbers are the entire bug. Any conversion that took longer than ten minutes outlived the session required to download its own result.

The access log

His repair ran from 12:41:27 to 12:49:11 local, 7 minutes and 44 seconds. That is 19:41 to 19:49 UTC, so his cookie was set around 19:41:2x and expired around 19:51:2x.

He clicked download at 19:51:23. The log line reads 403, size 39. He tried twice more, at 19:53:38 and 19:57:10, and got the same response both times.

Earlier in the same session he had downloaded a check report at 19:40:20 and it worked: 200, size 395. That request fell inside the ten minute window, which is why nothing looked broken until a slow job finished.

He was not the only one

Two other people hit the same 403 on the same day, on jobs at 13:33 and 16:38. Neither of them told us anything.

There is no reason they would have. They asked for a file and a file arrived.

The fix

EXEMPT_PATTERNS in CaptchaFilter now skips /api/convert/download/**. The job id is an unguessable UUID and the stored object expires on its own, so the gate was not buying protection there. Submitting a conversion is still gated, because that is the expensive operation.

Three tests lock the behaviour in, so the download route cannot quietly fall back under the broader pattern again.

The worse part came after the fix

He tried again about 48 minutes after we deployed, and still got the same captcha error. We pulled the deployed bundle with curl and confirmed the fix was live.

Delivery was the problem. Our Caddyfile set no Cache-Control header on anything at all. With no directive, browsers apply heuristic caching to index.html, and a cached shell keeps pointing at the previous build's content hashed bundle. Every returning visitor was running old JavaScript after every deploy. Our own post-deploy test passed because our cache happened to be fresh.

There is a second trap underneath that one, and it cost an hour. docker-compose mounts the Caddyfile as a single file bind mount, which binds the file's inode. git pull replaces a file by writing a new one and renaming it, which produces a new inode, so the container carried on serving the pre-pull config. The reload command reported success and silently re-applied the stale file. Force-recreating the container is the only thing that works.

While all of this was happening, the contact form he was trying to report it through was silently rejecting every logged out message. He reported the bug twice before one reached us. He also hit his daily conversion limit retrying a download that was never going to succeed.

The tool was lying too, in a smaller way

Weeks later the same person sent a different file, an epicycloid surface of 9,974 triangles. The repair produced a watertight solid and labelled it PRINTABLE. It had discarded most of the shape to get there.

The file is an open surface: two interleaved sheets sharing one bounding box, 4,006 open boundary edges, 0 non-manifold edges. Our shell filter kept both shells and dropped nothing. All of the loss happened inside pymeshfix, which has to invent a closure to produce a watertight result.

We tried the obvious setting. Turning off component joining makes it worse: the Z extent collapses from 19.05 to 5.71. No permutation we tested preserves this geometry, so the default stayed where it was.

What changed is what the tool says. When the output stops matching the input on shell count, face count or bounding box, the repair tool now reports REBUILT, CHECK IT AGAINST YOUR ORIGINAL, and lists the numbers that moved.

When a mesh has no errors and still cannot be repaired

The epicycloid file is worth pulling out of the story, because it is the case people go looking for after a repair finishes and the result is wrong. It had 0 non-manifold edges. Nothing a mesh checker flags was wrong with it.

What it had instead was 4,006 open boundary edges across two interleaved sheets sharing one bounding box. That is an open surface rather than a solid with holes in it. There is no correct way to close it, because the closure does not exist in the original model. Any tool that hands back a watertight solid has invented the missing volume and picked one interpretation out of many.

This is how a repair reports success and still returns the wrong shape. The output really is watertight. It is just not your part any more.

The measurement that separates the two cases is the open edge ratio. If a large share of a mesh's edges are boundary edges rather than edges shared between two triangles, the file is a surface and not a shell, and repairing it into a solid will change the geometry. We now warn on that in the inspection report, before the multi-minute reconstruction rather than after it, because the report is where someone with this file should start.

If you have one of these, a better repair tool is not the answer. Going back to whatever produced the surface and exporting a solid is. Failing that, thicken the surface deliberately and check the result against the original yourself, rather than trusting a watertight verdict.

What I take from this

Every bug here was in the scaffolding rather than the geometry. The mesh code did its job each time. What reached the user was the captcha, the cache headers, the contact form, and the wording of a verdict.

None of it appeared in our own testing, for a dull reason: our test conversions finish in seconds and our browsers always have a fresh cache. This failure needed a slow job and a returning visitor, and we had neither.

One forum reply produced three real bugs and a copy fix. If you have an STL that behaves strangely in either of our mesh tools, we would rather hear about it than not.

Check or repair an STL in the browser, no signup.

Open the STL repair tool

Rebuild a mesh as editable CAD geometry instead.

Open the STL to STEP converter

This guide covers one problem. For the full set of 3D converters, checkers, and calculators, visit the hub.

Browse 3D Tools