Better Fetch

· Paul Crossland

A Browser State File Is a Checkpoint, Not a Cookie Jar

Fresh IndexedDB and OPFS changes show why browser session exports need quiescence, coverage, and round-trip verification.

A browser session can export cleanly to JSON and still be impossible to restore correctly. The file may omit an origin-scoped store, capture a database while the page is still writing, or leave a database connection open so the restore waits forever. Worse, the browser engine itself may mishandle a failed storage mutation. None of those failures look like a bad cookie jar; they appear later as a logged-out shell, changed consent state, missing API calls, stale application data, or a crawler that never finishes.

Fresh browser and automation changes expose all three risks. A Playwright bug reported August 14 showed that storageState({ indexedDB: true }) in version 1.62.1 could leave its own IndexedDB connection open. A later setStorageState() on an open page at that origin could then wait indefinitely while trying to delete and recreate the database. The merged August 15 fix closes connections opened during collection and restoration. Separately, an open Playwright proposal from August 13 adds opt-in Origin Private File System capture, including binary files, nested directories, and empty directories.

The browser layer has its own failure domain. Safari Technology Preview 250, released August 13, fixes an IndexedDB put() that failed a unique-index constraint but deleted the record it would have overwritten, leaving neither the old nor new record.

The practical thesis is: a reusable browser state export should be operated as a versioned database checkpoint, with explicit surface coverage, a quiescence boundary, bounded capture and restore, and a verified round trip before it can support production fetch evidence.

This applies only to owned or properly authorized sessions. A state file may contain authentication material, personal data, consent choices, and application files. It should never be copied between users, tenants, or permission scopes to cross a login, challenge, payment, or other access boundary.

The repeated angle to avoid

Recent Better Fetch posts already cover cookie partitioning, consent state, authentication boundaries, stale service workers, session provenance, human-to-agent handoffs, and browser lifecycle telemetry. The repeated article would say that browser state matters or that storage should be logged.

The new surplus is a checkpoint protocol. State portability has at least three independent correctness questions: did the export cover every required storage surface, was that surface stable while it was read, and did restore produce an equivalent usable application state? A successful serialization call answers none of them by itself.

Source map

Fresh primary evidence from the last seven days:

SourceDateContribution
Playwright issue 422582026-08-14Reproduces an IndexedDB connection leak, blocked deletion, an unbounded setStorageState() wait, and fresh-context workarounds in Playwright 1.62.1.
Playwright pull request 422602026-08-15Merges explicit connection closure after IndexedDB collection and restore, plus a round-trip regression test.
Playwright pull request 422482026-08-13Proposes opt-in OPFS capture and restore, demonstrating that storage-state coverage is still expanding. It remains open, so it is not a released guarantee.
Safari Technology Preview 2502026-08-13Fixes an engine-level IndexedDB atomicity failure involving a rejected unique-index update. This is preview-channel evidence, not a claim about every stable Safari build.

Older background includes Playwright's authentication and storage-state guidance, the IndexedDB API, and the Origin Private File System. Together they explain why modern session state is an origin-scoped collection of databases and files, not only cookies and local storage.

Four properties of a trustworthy checkpoint

Coverage

Start with an allowlist of state surfaces the job actually requires. Cookies and local storage may be enough for one site. Another may put an auth token or application cache in IndexedDB. A web app can store binary or structured artifacts in OPFS. Session storage, Cache Storage, service-worker registrations, virtual credentials, and browser-managed permissions have different lifetimes and may not be present in a given export format.

An omitted surface must be reported as not_captured or unsupported, not silently interpreted as empty. The open OPFS work is a useful warning: yesterday's storageState schema and tomorrow's can both be valid JSON while representing different portions of a profile.

Quiescence

A checkpoint needs a moment at which writes are known to be complete. Waiting for a selector or load event does not prove that IndexedDB transactions, background workers, debounced settings, file writes, or service-worker updates have stopped.

Use an application-owned readiness marker where possible. Pause scheduled work in test fixtures, wait for known transactions and writable streams to finish, and reject capture if relevant activity continues beyond a budget. Do not freeze or inspect third-party sessions in ways the operator is not authorized to perform.

The Playwright connection leak shows a second part of quiescence: the capture tool must release the handles it opens. A read can finish successfully yet change the future behavior of the page by keeping a database connection alive.

Bounded completion

Give capture and restore their own deadlines and cancellation reason. The August 14 reproduction reported that the hanging restore was not affected by the context's default timeout. An outer job watchdog might eventually kill it, but that produces the wrong incident label unless the storage operation has its own span.

Record the phase that exceeded budget: origin discovery, database open, transaction read, database deletion, schema recreation, record write, OPFS traversal, file write, or semantic verification. A storage timeout should not become a navigation retry against the source.

Round-trip equivalence

Never validate a checkpoint only by parsing its file. Restore it into a disposable fresh context with the same supported browser and automation build, then export again and compare a normalized manifest. Finally, run a harmless semantic canary: does the authorized fixture reach the expected representation, preserve locale and consent class, and read the expected non-secret marker?

Byte equality is not always the right test because ordering and generated metadata can differ. Compare required origins, surface classes, database and store schemas, record counts, directory trees, bounded content hashes, and the application-level state the data product depends on.

A checkpoint manifest for fetch operations

Attach a manifest to every state artifact:

  • checkpoint_id, capture time, expiry, tenant, principal class, authorization scope, and intended task;
  • browser product, version, revision, profile mode, automation library version, and runtime image digest;
  • origin allowlist and per-origin surface status: captured, intentionally omitted, unsupported, failed, or unknown;
  • cookie count, local-storage key count, IndexedDB database/store/record counts, and OPFS file/directory counts, without broad logging of secret values;
  • application quiescence marker, last observed storage mutation, capture duration, timeout source, and handle-close result;
  • schema version, encryption state, artifact digest, retention class, and access-control policy;
  • restore build, restore duration, blocked/error events, post-restore manifest digest, and semantic-canary result;
  • final verdict: verified, partial, stale, restore_failed, policy_stop, or unknown.

State artifacts belong in a secret store, not ordinary crawl logs or source control. Prefer short retention, encryption, tenant isolation, audited reads, and metadata-only operational dashboards. Rotate or revoke the underlying session when a state artifact is exposed.

An owned-fixture checkpoint test

Build the test against an owned origin:

  1. Create cookies and local storage, an IndexedDB database with a unique index, and an OPFS tree containing text, binary, nested, and empty-directory cases.
  2. Keep a page open, export the supported surfaces, and verify that the capture tool closes every database connection it opened.
  3. Attempt a safe database deletion after capture. A blocked event should fail the canary rather than hang the suite.
  4. Restore into both a fresh context and, if production supports it, an already-open context. Bound each storage phase independently.
  5. Trigger a duplicate unique-index write and verify that the original fixture record survives the rejected transaction on every browser build you operate.
  6. Re-export and compare the normalized manifest plus a semantic application marker.
  7. Repeat while a controlled background write is active. The system should wait for the declared quiescence point or reject the checkpoint as inconsistent.
  8. Run the matrix before browser or automation promotion. Do not assume the merged Playwright fix is in your deployed package until the exact released build passes.

The operator decision rule

When a restored session produces different web data, compare checkpoint evidence before blaming the source or changing retries. If a required surface was omitted, classify the state as partial. If capture did not reach quiescence, discard it rather than guessing which generation won. If deletion or restore was blocked, fix the harness and close its handles. If a browser build fails the storage-integrity fixture, quarantine that build's stateful results. If the checkpoint round trip passes but the site returns a login, challenge, denial, or changed permission, preserve that policy outcome and stop.

A browser state file is convenient because it looks static. The system behind it is not. Databases have transactions and live connections; private file systems have trees and binary content; engines have version-specific storage behavior; and authorization can expire independently of every byte in the export. Treating state as a checkpoint makes those boundaries testable before a quietly incomplete session becomes quietly incorrect data.