From b4ac2fb821388270e4864f5536ffba7a21bc1678 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Mon, 13 Apr 2026 22:18:08 -0500 Subject: [PATCH] Fix broken service worker + device auth rejection on playlist refresh Bug 1 (SW): Rewrote service worker fetch handler: - Skip range requests (video seeking) to avoid caching partial responses - Skip non-GET requests entirely - Use ignoreSearch on cache match to avoid query-param misses - Don't cache opaque cross-origin responses - Outer catch on Cache API failures - Don't intercept catch-all requests (let browser handle natively) - Bump cache version to v4 to purge broken cached responses Bug 2 (auth): Playlist refresh register was missing device_token, causing auth rejection every 5 minutes. Fixed by including token in the refresh-register emit. Added diagnostic logging on both client and server for token validation failures. Co-Authored-By: Claude Opus 4.6 --- server/player/index.html | 6 +++- server/player/sw.js | 59 +++++++++++++++++++++++++++++---------- server/ws/deviceSocket.js | 2 +- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/server/player/index.html b/server/player/index.html index 27065f6..db367b0 100644 --- a/server/player/index.html +++ b/server/player/index.html @@ -421,6 +421,7 @@ }; // Browser fingerprint (survives localStorage clear) data.fingerprint = generateBrowserFingerprint(); + console.log(`[register] device_id=${data.device_id || 'none'}, has_token=${!!data.device_token}, token_len=${data.device_token?.length || 0}, paired=${config.paired}, pairing_code=${data.pairing_code || 'none'}`); socket.emit('device:register', data); } @@ -455,7 +456,10 @@ if (refreshTimer) clearInterval(refreshTimer); refreshTimer = setInterval(() => { if (socket?.connected && config.deviceId && config.paired) { - socket.emit('device:register', { device_id: config.deviceId, device_info: {} }); + const data = { device_id: config.deviceId, device_info: {} }; + if (config.deviceToken) data.device_token = config.deviceToken; + console.log(`[refresh-register] device_id=${config.deviceId}, has_token=${!!config.deviceToken}`); + socket.emit('device:register', data); } }, 300000); // 5 minutes fallback } diff --git a/server/player/sw.js b/server/player/sw.js index b71f7cb..9db8fbc 100644 --- a/server/player/sw.js +++ b/server/player/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'rd-player-v3'; +const CACHE_NAME = 'rd-player-v4'; const CONTENT_CACHE = 'rd-content-v1'; // Install: skip waiting to activate immediately @@ -19,18 +19,39 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); - // Content files (videos, images): cache on first fetch for offline playback + // Only handle GET requests — let POST/PUT/DELETE pass through + if (event.request.method !== 'GET') return; + + // Content files (videos, images): cache-first for offline playback if (url.pathname.startsWith('/uploads/content/')) { + // Skip range requests (video seeking) — serve from network, don't cache partial responses + if (event.request.headers.get('range')) { + return; // Let the browser handle range requests directly + } + event.respondWith( - caches.match(event.request).then(cached => { - if (cached) return cached; - return fetch(event.request).then(response => { - if (response.ok) { - const clone = response.clone(); - caches.open(CONTENT_CACHE).then(cache => cache.put(event.request, clone)); - } - return response; - }).catch(() => new Response('Offline', { status: 503 })); + caches.open(CONTENT_CACHE).then(cache => + cache.match(event.request, { ignoreSearch: true }).then(cached => { + if (cached) return cached; + return fetch(event.request).then(response => { + // Only cache successful, complete (non-opaque) responses + if (response.ok && response.status === 200 && response.type !== 'opaque') { + cache.put(event.request, response.clone()); + } + return response; + }).catch(() => { + return new Response('Content unavailable offline', { + status: 503, + statusText: 'Service Unavailable', + headers: { 'Content-Type': 'text/plain' } + }); + }); + }) + ).catch(() => { + // Cache API itself failed — fall through to network + return fetch(event.request).catch(() => + new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/plain' } }) + ); }) ); return; @@ -40,16 +61,24 @@ self.addEventListener('fetch', (event) => { if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') { event.respondWith( fetch(event.request).then(response => { - if (response.ok) { + if (response.ok && response.type !== 'opaque') { const clone = response.clone(); caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); } return response; - }).catch(() => caches.match(event.request).then(cached => cached || new Response('Offline', { status: 503 }))) + }).catch(() => + caches.match(event.request, { ignoreSearch: true }).then(cached => + cached || new Response('Offline', { + status: 503, + statusText: 'Service Unavailable', + headers: { 'Content-Type': 'text/plain' } + }) + ) + ) ); return; } - // Everything else: network only - event.respondWith(fetch(event.request)); + // Everything else: network only, don't intercept failures + // (Returning without calling event.respondWith lets the browser handle it natively) }); diff --git a/server/ws/deviceSocket.js b/server/ws/deviceSocket.js index e8f3efc..0b1316a 100644 --- a/server/ws/deviceSocket.js +++ b/server/ws/deviceSocket.js @@ -182,7 +182,7 @@ module.exports = function setupDeviceSocket(io) { if (device) { // Validate device token (skip for legacy devices that don't have a token yet) if (device.device_token && !validateDeviceToken(device_id, device_token)) { - console.warn(`Invalid device token for ${device_id} from ${getClientIp(socket)}`); + console.warn(`Invalid device token for ${device_id} from ${getClientIp(socket)} — received_len=${(device_token || '').length}, stored_len=${device.device_token.length}, received_prefix=${(device_token || '').substring(0, 8)}, stored_prefix=${device.device_token.substring(0, 8)}`); socket.emit('device:auth-error', { error: 'Invalid device token' }); return; }