ERR_CACHE_MISS / Confirm Form Resubmission (tested)
net::ERR_CACHE_MISS with the heading Confirm Form Resubmission appears when you go back or forward to a page that was the answer to a POST, and the browser has no stored copy. We reproduced it in Chromium 153, counted how often the server got the POST again, and tested the fixes.
1. What the error means
A page you get from submitting a form is the result of a POST. To show it again, the browser can either use a stored copy or send the POST again. Sending it again could repeat an order or a payment, so Chrome will not do that on back or forward. If it has no stored copy, it shows an error page instead.
This is the exact text Chromium 153 showed on back navigation in our test:
Confirm Form Resubmission
This web page requires data that you entered earlier in order to be
properly displayed. You can send this data again, but by doing so you
will repeat any action this page previously performed.
Press the reload button to resubmit the data needed to load the page.
ERR_CACHE_MISS
It is not a server error. Your server never sees a request when this page appears.
2. Our test setup
A Node server served a form at /form that POSTs to /submit. The server counted every POST. Playwright then did: submit, reload, go to another page, back, reload, forward, back. We changed only the POST response between runs.
In Playwright, the failed back navigation throws page.goBack: net::ERR_CACHE_MISS and the page URL becomes chrome-error://chromewebdata/. Note: the default Playwright headless shell renders an empty error page (39 bytes, no text). The full Chromium build in headless mode (channel: 'chromium') rendered the full text above.
3. Results: what back and reload do after a POST
| POST response | Back / forward to result | Reload of result | Total POSTs for 1 submit |
|---|---|---|---|
| 200, no Cache-Control | Shown from cache, 0 new POSTs | Re-POSTs (same body), no prompt in headless | 3 |
200, Cache-Control: no-cache | Shown from cache, 0 new POSTs | Re-POSTs | 3 |
200, Cache-Control: no-store | net::ERR_CACHE_MISS, Confirm Form Resubmission | Re-POSTs | 3 |
303 to /done (PRG) | GET page, 0 POSTs | GET /done, 0 POSTs | 1 |
302 to /done | GET page, 0 POSTs | GET /done, 0 POSTs | 1 |
307 to /done | From cache, 0 POSTs | Re-POSTs to /done | 4 |
200 + history.replaceState to /done | GET page, 0 POSTs | GET /done, 0 POSTs | 1 |
"Total POSTs" is over the whole run of submit, two reloads and three back/forward moves. Three findings stand out:
- no-store is what triggers ERR_CACHE_MISS. Without it (or with
no-cache), Chromium showed the stored copy on back.performancereportedback_forwardandpageshowhadpersisted=false, so this was the HTTP cache, not the back/forward cache. See no-cache vs no-store. - Reload always re-POSTs the result page. Every reload sent the POST again with
Cache-Control: max-age=0. Playwright'spage.reload()got no dialog event. A headed Chrome shows a "Confirm Form Resubmission" prompt first (per Chrome's behaviour, not measured here). - Reloading the no-store error page sent an empty POST. The request had
Content-Length: 0and noContent-Type. The form data was gone. Your handler should reject that, not process it.
4. The fix: Post/Redirect/Get with 303
Answer a successful POST with a redirect to a normal GET page. The history entry then holds the GET URL, not the POST.
// Node / Express
app.post('/order', async (req, res) => {
const id = await saveOrder(req.body);
res.redirect(303, `/order/${id}`); // See Other: browser follows with GET
});
<?php // PHP
save_order($_POST);
header('Location: /order/' . $id, true, 303);
exit;
With 303, our run sent exactly 1 POST. Two reloads and three back/forward moves all became GET /done. No ERR_CACHE_MISS. 302 behaved the same in Chromium. 307 does not work: it keeps the method, so Chromium sent POST /done and every reload re-POSTed. Per RFC 9110, 308 also keeps the method (not measured here). See 301 vs 302 vs 307 vs 308.
Use 303 for PRG. Per RFC 9110 it is the status meant for "go GET this other URL". If the redirect does not fire at all in PHP, see PHP header Location not working.
5. Does history.replaceState after the POST help?
Partly. We added this script to the 200 result page:
<script>history.replaceState(null, '', '/done');</script>
Chromium then treated the entry as a GET to /done. Reload and back sent GETs, and the run had 1 POST total. But two catches:
- The new URL must work as a GET. When we replaced with the same URL (
location.href, i.e./submit), reload and back sentGET /submitand our server answered 405. - It needs JavaScript and runs after the page loads. A 303 works for every client. Use replaceState only when you cannot change the server.
6. Other fixes and when to use them
- Remove
no-storefrom POST results if the page holds nothing sensitive. Back then works from cache. Reload still re-POSTs. - Use GET for searches and filters. They do not change anything, so put the fields in the query string. Back and reload are then always safe.
- Make POST handlers idempotent. Use a one-time token in a hidden field and ignore a second submit with the same token. This protects you even when a user forces a resubmit.
- Submit with fetch(). A form sent via
fetchdoes not create a POST history entry (per the HTML spec, not measured). Update the page or navigate to a GET URL afterwards.
As a user who just sees the error: press reload only if you are fine sending the form again. Otherwise go back to the form page and start over.
Frequently asked questions
What causes net::ERR_CACHE_MISS?
Going back or forward to a page that came from a POST when the browser has no stored copy. In our Chromium 153 test it happened when the POST response had Cache-Control: no-store. Without no-store the page came from cache.
Does pressing reload send the form again?
Yes. In our test every reload of a POST result page sent the POST again. Reloading the ERR_CACHE_MISS page itself sent a POST with an empty body (Content-Length: 0).
How do I stop Confirm Form Resubmission on my site?
Use Post/Redirect/Get. Answer the POST with 303 See Other to a GET URL. In our test that cut the server POST count from 3 to 1 across two reloads and three back/forward moves.
Can I use 307 or 308 for Post/Redirect/Get?
No. 307 and 308 keep the method. Chromium sent POST to the redirect target and re-POSTed on reload. Use 303. 302 also turned into GET in our test, but 303 is the one meant for this.
Does history.replaceState fix it?
It worked in our test if you replace the URL with one that answers GET. Replacing with the same POST URL made reload send GET /submit, which returned 405. A server-side 303 is more reliable.
Written with AI assistance. Measured on 2026-10-02 with Chromium 153.0.8010.12 driven by Playwright (both the default headless shell and the full 'chromium' channel in headless mode) against a Node.js 24.21.0 test server that counted every POST. The headed Chrome reload dialog was not measured. PlainHeaders is our extension; Chromium, Playwright and Node.js are free.