PWA/TWA does not respect HTTP cache of a "start_url" document on first open

#505481952 Filed 2026-04-23 Blink>Network P3 / S3 Reproduces on Android Fix verified on device

What happens

The reporter's setup: a PWA whose start_url is served with cache-control: private, max-age=1209600, and the document replaces itself with a newer one via location.reload(). While the app is running everything looks right. Kill it (swipe from recents or force-stop), launch it again, and Chrome renders the old document out of the HTTP cache. I could only reproduce this on Android; desktop Chrome always came back with the fresh document.

Why

There is no clean shutdown on Android. The app gets a STOPPED signal when it leaves the screen, and whatever happens afterwards (freezer, SIGKILL) gives the process no further chance to write anything. Both HTTP cache backends keep part of a written entry in memory for as long as the entry is open: SimpleCache holds the headers (stream 0) plus the EOF/CRC records and only writes them when the entry closes; the SQL backend buffers header and body writes in the entry object and flushes when it is destroyed. In this repro the entry for the displayed start_url document is still open when the kill arrives, so the fresh copy never reaches disk and the stale one wins the next cold launch.

Fix

CL 8051344 hooks the same STOPPED signal the SimpleCache index already uses to save itself (INDEX_WRITE_REASON_ANDROID_STOPPED) and checkpoints every open entry: buffered headers and data go to disk, the entry stays open and usable. On a Pixel 6a with the patched APK, a cold relaunch after force-stop shows the fresh document with zero network requests, and the on-disk entry files are md5-identical before and after the kill. Deterministic unit tests cover both backends.

Try it

python3 repro_server.py --port 8000 --reload-delay 4000
adb reverse tcp:8000 tcp:8000     # or any public HTTPS tunnel

# stock Chrome: install the PWA, open it (red V1 shows, reloads to green V2),
# force-stop, cold-launch the installed app
#   -> red V1 shows again for 4s = the bug
# with ChromePublic-fixed.apk
#   -> green V2 immediately, zero requests

Don't trust the screen alone

The stale V1 self-heals through its own reload, so with no delay the bug is invisible on screen. The reliable signal on a cold relaunch is the combination of sessionStorage.reloaded == 1 (the V1 bootstrap ran) and exactly one GET / in the server log (V1's reload). Zero requests and no flag means the cache served fresh V2 directly, which is what the patched build does.

Downloads

⬇ repro_server.py ⬇ ChromePublic-fixed.apk (arm64, 911M) Issue 505481952