The Fix That Could Not Reach the Phone
On Sunday I published a fix: four lines that make a missing stylesheet fail as a missing stylesheet instead of being answered with the homepage. It was right. That afternoon the app came up unstyled on the phone again.
The four lines had done their job. No new request for a dead stylesheet was getting HTML back; the access log showed clean 404s where the 200s had been. What they could not do was reach the copy of the homepage that Safari had already filed under the stylesheet’s URL days earlier. A server-side guard is a rule about future answers. The phone was not asking. It had the answer, and it kept rendering from it, and a tab restored from the background renders from cache without revalidating anything at all.
So the guard was the last step of a chain, and I had fixed the last step. The first step was the one that mattered. Every rebuild of the client had been running straight into the directory the live server serves from. The build tool empties that directory first, then writes the new files, which leaves a window of a few seconds with no assets on disk at all. A phone that loads during the window asks for its stylesheet, the file is not there yet, and the fallback answers with the page. Before Sunday’s guard that answer was a 200 and Safari kept it. The window was the poisoner. The fallback was the syringe. The guard sat between them and could only stop the second.
The fix that holds is three layers, none of them the four lines. The build now writes into a staging directory and swaps it in with one rename, so the served directory is never empty or partial, and the window does not exist. The page itself is served with no-store, and the hashed assets are served as immutable for a year, which is the split they should always have had: the file whose name never changes must never be cached, and the files whose names change with every build can be cached forever. And every response carries nosniff, so a body of the wrong type can never be applied as a stylesheet again. If some future hole hands a browser HTML at a CSS address, it fails on the spot instead of poisoning the cache for a week.
The essay on Sunday was about the fallback. The fallback was real and the fix to it stands. But I had written up the step I could see in the log, and the log was on the server, and the server was the only place in the chain where nothing had gone wrong on its own.