mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-17 07:34:15 -06:00
Serve the service worker from the root, so its scope needs no header to survive
Found deploying 1.9.29 to production. A worker's scope defaults to its own directory, so /player/sw.js could only control /player/ and below; the fix was to request a wider scope and permit it with Service-Worker-Allowed. That works right up until something between the origin and the browser does not pass the header on. Cloudflare served a CACHED response for that path across the deploy — headers and all — and the registration failed outright. A rejected registration is worse than a narrow one: the player runs with no worker at all, on every URL, and nothing about it is visible from the server. The origin was sending the header correctly the whole time; a cache-busted request proved it. It self-heals when the edge entry expires, which is precisely the kind of fix nobody should have to know about. Served from /, the default scope is already the whole origin and no header has to survive the trip — through Cloudflare, through whatever a self-hoster puts in front of it, or through a corporate proxy we will never see. /player/sw.js keeps serving for players still asking for it, and the header is still sent where it does survive. Verified in a real browser: all three of /player, /player/ and /player/index.html are controlled from root scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
parent
3b9ad08454
commit
c2240288a7
|
|
@ -4103,7 +4103,12 @@
|
||||||
// with. Registration succeeded there and then controlled nothing: no shell cache, no content
|
// with. Registration succeeded there and then controlled nothing: no shell cache, no content
|
||||||
// cache, no offline playback, and no error to notice. The server sends
|
// cache, no offline playback, and no error to notice. The server sends
|
||||||
// Service-Worker-Allowed so this wider scope is permitted.
|
// Service-Worker-Allowed so this wider scope is permitted.
|
||||||
navigator.serviceWorker.register('/player/sw.js', { scope: '/' }).then(reg => {
|
// Registered from the ROOT, where the default scope already covers the whole origin. The
|
||||||
|
// previous form asked for a wider-than-default scope and relied on a Service-Worker-Allowed
|
||||||
|
// header reaching the browser — which Cloudflare withheld from a cached response across a
|
||||||
|
// deploy, so registration failed and the player ran with no worker at all. A rejected
|
||||||
|
// registration is worse than a narrow one, and nothing about it is visible from here.
|
||||||
|
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||||||
console.log('Service Worker registered');
|
console.log('Service Worker registered');
|
||||||
// When a new SW activates, reload so the fresh code takes effect immediately
|
// When a new SW activates, reload so the fresh code takes effect immediately
|
||||||
reg.addEventListener('updatefound', () => {
|
reg.addEventListener('updatefound', () => {
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,28 @@ app.get('/player/schedule-eval.js', (req, res) => {
|
||||||
// tests via require — one source, so the range arithmetic the player depends on cannot drift from
|
// tests via require — one source, so the range arithmetic the player depends on cannot drift from
|
||||||
// the arithmetic that is actually tested. A service worker cannot require(), which is why this is
|
// the arithmetic that is actually tested. A service worker cannot require(), which is why this is
|
||||||
// a served file rather than a bundled one.
|
// a served file rather than a bundled one.
|
||||||
|
/*
|
||||||
|
* The service worker, at the ROOT.
|
||||||
|
*
|
||||||
|
* A worker's scope defaults to its own directory, so /player/sw.js can only control /player/ and
|
||||||
|
* below — not /player itself, which is the URL the dashboard shows. The fix for that was to ask for
|
||||||
|
* a wider scope and permit it with a Service-Worker-Allowed header, and it works... until something
|
||||||
|
* in front of the origin does not pass the header on. Cloudflare served a CACHED response for that
|
||||||
|
* path across a deploy, headers and all, and the registration failed outright: worse than the
|
||||||
|
* narrow scope it replaced, because a rejected registration means no worker at all.
|
||||||
|
*
|
||||||
|
* Served from / instead, the default scope IS the whole origin and no header is required. That
|
||||||
|
* removes the dependency on a custom response header surviving every CDN, proxy and cache between
|
||||||
|
* us and a display — including the ones self-hosters run and we will never see.
|
||||||
|
*
|
||||||
|
* /player/sw.js keeps working for players still asking for it.
|
||||||
|
*/
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Service-Worker-Allowed', '/'); // belt: harmless, and correct where it survives
|
||||||
|
res.sendFile(path.join(__dirname, 'player', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/player/cache-policy.js', (req, res) => {
|
app.get('/player/cache-policy.js', (req, res) => {
|
||||||
res.type('application/javascript').setHeader('Cache-Control', 'no-cache');
|
res.type('application/javascript').setHeader('Cache-Control', 'no-cache');
|
||||||
res.sendFile(path.join(__dirname, 'lib', 'player-cache-policy.js'));
|
res.sendFile(path.join(__dirname, 'lib', 'player-cache-policy.js'));
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,16 @@ process.env.NODE_ENV = 'test';
|
||||||
const { test } = require('node:test');
|
const { test } = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
test('the worker is registered with an explicit root scope', () => {
|
test('the worker is registered from the root, so its DEFAULT scope covers the player', () => {
|
||||||
// Without {scope:'/'} the registration silently narrows to /player/ and the page at /player is
|
// Asking for a wider-than-default scope works only if Service-Worker-Allowed reaches the browser.
|
||||||
// left uncontrolled.
|
// Cloudflare withheld it from a cached response across a deploy and registration failed outright
|
||||||
|
// — no worker at all, which is worse than the narrow scope it replaced. Served from /, the
|
||||||
|
// default scope is already the whole origin and no header has to survive the trip.
|
||||||
const html = fs.readFileSync(path.join(__dirname, '..', 'player', 'index.html'), 'utf8');
|
const html = fs.readFileSync(path.join(__dirname, '..', 'player', 'index.html'), 'utf8');
|
||||||
assert.match(html, /navigator\.serviceWorker\.register\('\/player\/sw\.js',\s*\{\s*scope:\s*'\/'\s*\}/,
|
assert.match(html, /navigator\.serviceWorker\.register\('\/sw\.js'\)/,
|
||||||
"register() must ask for a scope wider than the worker's own directory");
|
'register the worker from the root rather than relying on a header');
|
||||||
|
const server = fs.readFileSync(path.join(__dirname, '..', 'server.js'), 'utf8');
|
||||||
|
assert.match(server, /app\.get\('\/sw\.js'/, 'and the server must serve it there');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the server permits that scope, or the registration is rejected outright', async () => {
|
test('the server permits that scope, or the registration is rejected outright', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue