◉CLAUDE LABJP
●2.1.311 — Adds idle-hold options for self-hosted runners and fixes startup freezes and stalled interrupts (10/26)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 33 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…●2.1.311 — Adds idle-hold options for self-hosted runners and fixes startup freezes and stalled interrupts (10/26)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 33 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-10-28Advanced

Back From Checkout, the Button Stayed on Processing: Why I Fixed It With pageshow, and the 3 Files My Audit Still Found

When you press Back from Stripe or another external page, a checkout button can stay stuck on Processing. I reproduce the bfcache cause in a real browser, compare pageshow with no-store, and add a repo audit script and a CLAUDE.md rule.

Claude Code276bfcacheStripe16Playwright3CLAUDE.md6

✦ Premium Article

You open the checkout page, then press the browser's Back button. The button you land on is greyed out and still says "Processing…". No matter how often you press it, nothing happens.

I ran into this on the membership page of a small project I run as an indie developer. A reload fixes it, which is exactly why it is easy to miss. Someone who hesitated, went back, and found a dead button might simply leave.

The API call had not failed, either. The logs offer no clue for this symptom.

What happened: the page was frozen, then thawed

When you navigate away, the browser sometimes keeps the old page in memory in a place called bfcache (the Back-Forward Cache). On Back it thaws the page instead of loading it again.

React components are not remounted. The useState value stays as it was, so loading is still true. If the button says disabled={loading !== null}, it stays disabled.

A button that navigates to an external service has to be written for "the version of me that comes back", or its state stays stranded in the past.

The clue is persisted on the pageshow event. It is true only when the page was restored from bfcache.

First, I reproduced it in a real browser

Judging by feel makes "it's fixed" a weak claim. So I rebuilt the same structure without React, using just a loading variable and a label, and drove "press, navigate away, go back" with Playwright.

// bfcache-check.js — check in a real browser that the checkout button does not freeze on Back
// Usage: node bfcache-check.js   (Playwright must be installed)
const http = require('http');
const { chromium } = require('playwright');
 
// Test page: a loading variable and a label reproduce the same structure as the React component
const page_html = (guard, payUrl) => `<!doctype html><meta charset="utf-8"><title>plans</title>
<button id="join">Join</button><p id="state">idle</p><p id="evt">none</p>
<script>
  let loading = false;
  const $ = (id) => document.getElementById(id);
  $('join').onclick = async () => {
    loading = true; $('state').textContent = 'loading';
    await new Promise((r) => setTimeout(r, 50));      // stands in for the /api/checkout round trip
    location.href = '${payUrl}';                        // navigate to Stripe
  };
  addEventListener('pageshow', (e) => {
    $('evt').textContent = 'persisted=' + e.persisted;
    ${guard ? "if (e.persisted) { loading = false; $('state').textContent = 'idle'; }" : ''}
  });
</script>`;
 
const serve = (port, handler) =>
  new Promise((ok) => { const s = http.createServer(handler).listen(port, () => ok(s)); });
 
const CASES = [
  { name: 'A no guard',                guard: false, noStore: false },
  { name: 'B reset on pageshow',         guard: true,  noStore: false },
  { name: 'C no guard + no-store',     guard: false, noStore: true  },
];
 
(async () => {
  let failed = 0;
  for (const c of CASES) {
    const pay  = await serve(4102, (_, r) => { r.writeHead(200, { 'content-type': 'text/html' }); r.end('<title>pay</title>pay'); });
    const html = page_html(c.guard, 'http://localhost:4102/pay');
    const site = await serve(4101, (_, r) => {
      const h = { 'content-type': 'text/html' };
      if (c.noStore) h['cache-control'] = 'no-store';
      r.writeHead(200, h); r.end(html);
    });
    // Playwright disables bfcache by default, so drop that flag at launch
    const browser = await chromium.launch({ channel: 'chromium', ignoreDefaultArgs: ['--disable-back-forward-cache'] });
    const page = await browser.newPage();
    await page.goto('http://localhost:4101/');
    await page.click('#join');
    await page.waitForURL(/\/pay/);
    // page.goBack() waits for a load event that never fires on bfcache restore, so use history.back()
    await page.evaluate(() => history.back());
    await page.waitForTimeout(1200);
    const state = await page.textContent('#state');
    const evt = await page.textContent('#evt');
    const ok = state === 'idle';
    if (!ok) failed++;
    console.log(`${ok ? 'PASS' : 'FAIL'}  ${c.name.padEnd(22)} state=${state.padEnd(8)} ${evt}  (Chromium ${browser.version()})`);
    await browser.close(); site.close(); pay.close();
  }
  process.exit(failed ? 1 : 0);
})();

These are the results with Playwright 1.63.0 and Chromium 153.

FAIL  A no guard             state=loading  persisted=true  (Chromium 153.0.8010.12)
PASS  B reset on pageshow    state=idle     persisted=true  (Chromium 153.0.8010.12)
PASS  C no guard + no-store  state=idle     persisted=false  (Chromium 153.0.8010.12)

Case A is the symptom I saw in production: right after returning, persisted=true but the label still said loading.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You can reproduce the 'stuck on Processing after Back' symptom in a real browser and confirm for yourself that your fix actually worked
✦You can decide between resetting state on pageshow and stopping bfcache with Cache-Control: no-store, based on Back-navigation speed and how easily the bug returns
✦You can find, with an audit script, where the same hole is still hiding in your own code and close it before a bug report arrives
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Claude Code2026-10-20
You Stepped Away and Came Back to a Summary — Spotting Idle Compaction and Keeping the Details You Can't Lose
Since v2.1.299, Claude Code compacts a long conversation after about 54 idle minutes. Here is how to tell it happened, how to tune or switch it off, and how to move the assumptions you can't afford to lose into CLAUDE.md.
⟐ Claude Code2026-10-19
The Page Was 90px Too Wide at 375px, and the Culprit Never Showed Up in querySelectorAll
When a pseudo-element causes horizontal overflow on phones, scanning elements finds nothing. A probe script that confirms the culprit by diff, and overflow fixes that keep sticky intact, with measurements.
⟐ Claude Code2026-10-27
Counting How Often Claude Code Ships, Straight from npm's Publish History
How many Claude Code versions ship per week, and how many hours apart? A 37-line Python script reads npm's publish history to count weekly releases and the median gap, including the empty-week trap and why a Tuesday run makes this week look quiet.