Skip to content

[Bug]: WebKit 26.6 IndexedDB read stalls after cross-site back navigation when a service worker controls the page #42694

Description

@MetinGames

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:

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.

// 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 = `<!doctype html><html><body>
<p id="status">loading</p><a href="https://example.test/">Other site</a>
<script type="module">
const 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'));" : ''}
</script></body></html>`;
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: '<h1>Local synthetic destination</h1>'
  }));
  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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions