RESOLVED FIXED324058
IndexedDB transactions never start after cross-site back navigation when a service worker controls the page (regression from bug 322386)
https://bugs.webkit.org/show_bug.cgi?id=324058
Summary IndexedDB transactions never start after cross-site back navigation when a se...
Ashraf Ali
Reported 2026-09-12 12:47:13 PDT
After a page with a controlling service worker navigates cross-site and the user navigates back, IDBFactory.open() succeeds but the first transaction on the new connection never runs - its requests stay pending indefinitely. Without a service worker, or before commit 03efc9761a39, the same round trip works. STEPS TO REPRODUCE (self-contained, no external network needed): 1. npm install @playwright/test@1.63.0 && npx playwright install webkit (this build is WebKit 26.6 / r2359; current main r2361 is also affected) 2. Save the script below as repro.mjs and run: node repro.mjs 3. Observe: it prints {"result":"FAIL","phase":"read after back navigation"} - after goBack(), the page's #status stays at "loading" because the read transaction never completes within 7s. 4. Controls: node repro.mjs --no-sw (no service worker) prints PASS, and @playwright/test@1.62.1 (WebKit 26.5) prints PASS with the worker. --- repro.mjs --- // Standalone reproduction. Loopback HTTP server, IndexedDB with one key, // and a service worker whose only handler is activate -> clients.claim(). // The https://example.test/ destination is fulfilled via route interception, // so nothing leaves the machine. 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, 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)); } --- end repro.mjs --- EXPECTED: after the cross-site round trip, the transaction completes and returns the previously saved value. ACTUAL: after navigating back, database opening succeeds but the transaction never completes; the page stays at "loading". Additional controls that still fail after back navigation (i.e. they do not rescue it): - 127.0.0.1 -> localhost as the destination - history.back() instead of page.goBack() - Explicitly closing the database before leaving the page - Waiting for serviceWorker.ready before opening the database BISECT across published Playwright WebKit builds (reproduced identically on Windows and Linux): build WebKit service worker result r2336 26.5 yes PASS r2355 26.5 yes PASS r2358 26.6 yes FAIL r2359 26.6 yes FAIL r2359 26.6 no PASS r2361 26.6 yes FAIL (WebKit main as of Sep 2026) SUSPECT: commit 03efc9761a39 (bug 322386, landed 2026-08-26, inside the last-good/first-bad window). That commit changed UniqueIDBDatabase scheduling so takeNextRunnableTransaction() no longer picks transactions of suspended clients - they are kept queued until the client resumes, with resume handled via setClientSuspended(false) from the WebProcess. This scenario has all the ingredients: the cross-site navigation parks the page's client as suspended (back/forward cache), the page is restored on back navigation, and with a controlling service worker the queued transaction apparently never gets scheduled again - consistent with the resume notification never reaching the storage scheduler on this path. Without the worker the same round trip passes, so the plain resume path works. Original report and discussion: https://github.com/microsoft/playwright/issues/42694
Attachments
Radar WebKit Bug Importer
Comment 1 2026-09-13 22:53:22 PDT
Devin Rousso
Comment 2 2026-09-15 12:22:49 PDT
EWS
Comment 3 2026-09-17 19:11:12 PDT
Committed 321356@main (497098e97dcc): <https://commits.webkit.org/321356@main> Reviewed commits have been landed. Closing PR #74082 and removing active labels.
Note You need to log in before you can comment on or make changes to this bug.