microsoft / microsoft/playwright
[Bug]: WebKit 26.6 IndexedDB read stalls after cross-site back navigation when a service worker controls the page
@dcrousso is already working on this.
Since Sep 15, 2026.
- Dominant language
- TypeScript
- Stars
- 96.3k
- Forks
- 6.5k
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 180
Description
### Version
1.63.0 (WebKit 26.6 revision 2359); control: 1.62.1 (WebKit 26.5 revision 2336)
### Steps to reproduce
The complete standalone reproduction below uses only a loopback HTTP server, one database/object store/key and a service worker that claims clients. The worker has no fetch handler, cache, IndexedDB access or background work. It contains no application code, account, credential or production request. The `https://example.test/` destination is fulfilled locally.
Save the code as `Playwright-WebKit-sentetik-ornek.mjs` in an isolated directory, then run:
```powershell
npm install @playwright/test@1.63.0
npx playwright install webkit
node Playwright-WebKit-sentetik-ornek.mjs
node Playwright-WebKit-sentetik-ornek.mjs --no-sw
```
The first run fails during the read after back navigation; the no-worker control passes. Repeat with Playwright 1.62.1 and its bundled WebKit as the passing earlier-version control.
```javascript
// Standalone synthetic reproduction. No Nardora account, game code or production request.
// npm install @playwright/test@1.63.0 && npx playwright install webkit
// node Playwright-WebKit-sentetik-ornek.mjs
// Control: repeat with --no-sw, or @playwright/test 1.62.1 and its WebKit.
// PLAYWRIGHT_PACKAGE may point to an already installed @playwright/test directory.
import { createServer } from 'node:http';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const packagePath = process.env.PLAYWRIGHT_PACKAGE || '@playwright/test';
const { webkit, expect } = require(packagePath);
const withWorker = !process.argv.includes('--no-sw');
const html = `
loading
Other siteconst ready = new Promise((resolve, reject) => {
const request = indexedDB.open('synthetic-roundtrip', 1);
request.onupgradeneeded = () => request.result.createObjectStore('items');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
async function transaction(value) {
const db = await ready;
return new Promise((resolve, reject) => {
const tx = db.transaction('items', 'readwrite');
const request = tx.objectStore('items').get('name');
request.onsuccess = () => {
if (value) tx.objectStore('items').put(value, 'name');
};
tx.oncomplete = () => resolve(request.result);
tx.onerror = tx.onabort = () => reject(tx.error);
});
}
transaction().then(() => { document.querySelector('#status').textContent = 'ready'; });
window.save = () => transaction('Synthetic Player');
window.read = () => transaction();
${withWorker ? "addEventListener('load', () => navigator.serviceWorker.register('/worker.js'));" : ''}
`;
const server = createServer((request, response) => {
if (request.url === '/worker.js') {
response.writeHead(200, { 'Content-Type': 'application/javascript' });
// No fetch handler, cache, IndexedDB, game code or background work in this worker.
response.end("self.addEventListener('activate', event => event.waitUntil(self.clients.claim()));");
} else {
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(html);
}
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const browser = await webkit.launch();
const page = await browser.newPage();
const base = `http://127.0.0.1:${server.address().port}/`;
let phase = 'initial read';
try {
await page.route('https://example.test/', route => route.fulfill({
contentType: 'text/html', body: '
Local synthetic destination
'}));
await page.goto(base);
await expect(page.locator('#status')).toHaveText('ready', { timeout: 7000 });
if (withWorker) {
await page.evaluate(() => navigator.serviceWorker.ready);
await page.waitForFunction(() => navigator.serviceWorker.controller, null, { timeout: 7000 });
}
await page.evaluate(() => window.save());
await page.locator('a').press('Enter');
await expect(page).toHaveURL('https://example.test/');
phase = 'read after back navigation';
await page.goBack();
await expect(page.locator('#status')).toHaveText('ready', { timeout: 7000 });
expect(await page.evaluate(() => window.read())).toBe('Synthetic Player');
console.log(JSON.stringify({ result: 'PASS', engine: browser.version(), withWorker, phase }));
} catch (error) {
console.log(JSON.stringify({ result: 'FAIL', engine: browser.version(), withWorker, phase,
message: error.message.split('\n').slice(0, 7).join(' ') }));
process.exitCode = 1;
} finally {
await browser.close();
await new Promise(resolve => server.close(resolve));
}
```
### Expected behavior
After a cross-site round trip, the IndexedDB transaction completes and returns the previously saved synthetic value.
### Actual behavior
After navigating back, database opening succeeds but the transaction does not complete within the unchanged seven-second expectation. The page remains at `loading`. Removing the service worker or using Playwright 1.62.1/WebKit 26.5 makes the same round trip pass.
### Additional context
Additional isolated controls on 12 September 2026:
| Control | Result |
|---|---|
| Another port on 127.0.0.1, no route interception | PASS |
| Another port on 127.0.0.1, intercepted destination | PASS |
| 127.0.0.1 to localhost, intercepted destination | FAIL after back |
| HTTP example.test instead of HTTPS | FAIL after back |
| Remove the destination route before back | FAIL after back |
| Wait for serviceWorker.ready before opening the database | FAIL after back |
| history.back() instead of page.goBack() | FAIL after back |
| Explicitly close the database before leaving the page | FAIL after back |
This narrows the trigger to a cross-site round trip with a controlling service worker; it is not evidence that route interception alone is the cause. A real external-destination control was inconclusive because navigation failed before the back step.
We have not identified a WebKit source commit or tested a physical Safari release. We have not disabled service workers, reset storage or weakened the application's atomic storage contract to make the candidate pass. Please advise whether this belongs to Playwright's WebKit integration or should be tracked in WebKit, and whether there is a supported fixed build to validate.
### Environment
```shell
OS: Windows
Node.js: 24.18.0
@playwright/test: 1.63.0
Bundled WebKit: 26.6, revision 2359
Control: @playwright/test 1.62.1; WebKit 26.5, revision 2336
Physical Safari: not tested
```
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.