Version
1.63.0-next (reproduced on main at 46cd500; feature added in #42248)
Steps to reproduce
Two related problems with the new OPFS support. Both reproduce with the bundled test server.
1. A storage state carrying opfs: [] cannot be restored in WebKit.
const origin = 'http://localhost:PORT';
const withEmptyOpfs = { cookies: [], origins: [{ origin, localStorage: [{ name: 'k', value: 'v' }], opfs: [] }] };
const withoutOpfs = { cookies: [], origins: [{ origin, localStorage: [{ name: 'k', value: 'v' }] }] };
await webkitBrowser.newContext({ storageState: withEmptyOpfs }); // throws
await webkitBrowser.newContext({ storageState: withoutOpfs }); // fine
2. storageState({ opfs: true }) throws on WebKit for every origin.
await webkitContext.storageState({ opfs: true });
Expected behavior
For 1, restoring an empty OPFS list is a no-op, so it should behave the same as omitting the field.
This matters because opfs: [] is what the other engines actually write. Capturing on Chromium and
reusing the file on WebKit is a normal workflow.
For 2, given the docs say "OPFS is currently not supported in ephemeral WebKit contexts"
(class-browsercontext.md:1594), capture should skip OPFS rather than fail the call.
Actual behavior
1. Identical states, differing only in that one field:
opfs: [] -> THREW: browser.newContext: Error setting storage state:
Error: Unable to restore OPFS: The operation failed for an unknown transient reason (e.g. out of memory).
opfs absent -> OK, localStorage=v
And opfs: [] is exactly what the other engines produce for an origin with no OPFS files. Capturing
an origin that has localStorage but no OPFS gives:
browser=chromium originKeys=["origin","localStorage","indexedDB","opfs"] opfs=[]
browser=firefox originKeys=["origin","localStorage","indexedDB","opfs"] opfs=[]
2. On WebKit:
Error: browserContext.storageState: Error: Unable to serialize OPFS:
The operation failed for an unknown transient reason (e.g. out of memory).
The whole call fails, including origins that never touched OPFS.
Additional context
For 1, the guard at packages/injected/src/storageScript.ts:226-237 looks like it already intends
the behaviour I would expect, but tests the wrong thing:
} catch (e) {
// OPFS may be unavailable, e.g. on insecure origins or in WebKit contexts
// that fail with 'unknown transient reason'. There is nothing to clear then,
// so only fail when there are entries to restore.
if (originState?.opfs === undefined)
return;
throw e;
}
The comment says "only fail when there are entries to restore", but the check is
=== undefined rather than emptiness, and opfs: [] has no entries. The protocol declares
opfs: array? (playwright.yml:353), so an empty array is a legal value distinct from an absent one.
For 2, collect() at storageScript.ts:183-189 wraps getDirectory() and rethrows as
Unable to serialize OPFS. On the server side the live-page loop swallows per-origin failures, but
the fallback blank-page loop in browserContext.ts has no equivalent catch, so it propagates. I am
less sure this one is unintended, since IndexedDB already threw the same way before this feature, so
treat it as the weaker of the two.
I have not opened a PR. My PRs here keep getting closed over an unsigned CLA on my side, so this is
more useful to you as a report. Happy to send a patch if that changes.
the second half is the part I would push back on myself, since it matches how IndexedDB already
behaved and may well be deliberate. the first one I am confident about, the comment and the code
disagree. freshman in college, thanks for reading :)
Environment
System:
OS: macOS 26.5.2
CPU: (10) arm64 Apple M4
Binaries:
Node: 26.0.0
npm: 11.12.1
npmPackages:
playwright monorepo at main (1.63.0-next)
Version
1.63.0-next (reproduced on main at 46cd500; feature added in #42248)
Steps to reproduce
Two related problems with the new OPFS support. Both reproduce with the bundled test server.
1. A storage state carrying
opfs: []cannot be restored in WebKit.2.
storageState({ opfs: true })throws on WebKit for every origin.Expected behavior
For 1, restoring an empty OPFS list is a no-op, so it should behave the same as omitting the field.
This matters because
opfs: []is what the other engines actually write. Capturing on Chromium andreusing the file on WebKit is a normal workflow.
For 2, given the docs say "OPFS is currently not supported in ephemeral WebKit contexts"
(
class-browsercontext.md:1594), capture should skip OPFS rather than fail the call.Actual behavior
1. Identical states, differing only in that one field:
And
opfs: []is exactly what the other engines produce for an origin with no OPFS files. Capturingan origin that has
localStoragebut no OPFS gives:2. On WebKit:
The whole call fails, including origins that never touched OPFS.
Additional context
For 1, the guard at
packages/injected/src/storageScript.ts:226-237looks like it already intendsthe behaviour I would expect, but tests the wrong thing:
The comment says "only fail when there are entries to restore", but the check is
=== undefinedrather than emptiness, andopfs: []has no entries. The protocol declaresopfs: array?(playwright.yml:353), so an empty array is a legal value distinct from an absent one.For 2,
collect()atstorageScript.ts:183-189wrapsgetDirectory()and rethrows asUnable to serialize OPFS. On the server side the live-page loop swallows per-origin failures, butthe fallback blank-page loop in
browserContext.tshas no equivalent catch, so it propagates. I amless sure this one is unintended, since IndexedDB already threw the same way before this feature, so
treat it as the weaker of the two.
I have not opened a PR. My PRs here keep getting closed over an unsigned CLA on my side, so this is
more useful to you as a report. Happy to send a patch if that changes.
the second half is the part I would push back on myself, since it matches how IndexedDB already
behaved and may well be deliberate. the first one I am confident about, the comment and the code
disagree. freshman in college, thanks for reading :)
Environment