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 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-13 22:18:08 -05:00
parent dc7450b6a7
commit b4ac2fb821
3 changed files with 50 additions and 17 deletions

View file

@ -421,6 +421,7 @@
}; };
// Browser fingerprint (survives localStorage clear) // Browser fingerprint (survives localStorage clear)
data.fingerprint = generateBrowserFingerprint(); 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); socket.emit('device:register', data);
} }
@ -455,7 +456,10 @@
if (refreshTimer) clearInterval(refreshTimer); if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(() => { refreshTimer = setInterval(() => {
if (socket?.connected && config.deviceId && config.paired) { 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 }, 300000); // 5 minutes fallback
} }

View file

@ -1,4 +1,4 @@
const CACHE_NAME = 'rd-player-v3'; const CACHE_NAME = 'rd-player-v4';
const CONTENT_CACHE = 'rd-content-v1'; const CONTENT_CACHE = 'rd-content-v1';
// Install: skip waiting to activate immediately // Install: skip waiting to activate immediately
@ -19,18 +19,39 @@ self.addEventListener('activate', (event) => {
self.addEventListener('fetch', (event) => { self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url); 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/')) { 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( event.respondWith(
caches.match(event.request).then(cached => { caches.open(CONTENT_CACHE).then(cache =>
if (cached) return cached; cache.match(event.request, { ignoreSearch: true }).then(cached => {
return fetch(event.request).then(response => { if (cached) return cached;
if (response.ok) { return fetch(event.request).then(response => {
const clone = response.clone(); // Only cache successful, complete (non-opaque) responses
caches.open(CONTENT_CACHE).then(cache => cache.put(event.request, clone)); if (response.ok && response.status === 200 && response.type !== 'opaque') {
} cache.put(event.request, response.clone());
return response; }
}).catch(() => new Response('Offline', { status: 503 })); 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; return;
@ -40,16 +61,24 @@ self.addEventListener('fetch', (event) => {
if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') { if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') {
event.respondWith( event.respondWith(
fetch(event.request).then(response => { fetch(event.request).then(response => {
if (response.ok) { if (response.ok && response.type !== 'opaque') {
const clone = response.clone(); const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
} }
return response; 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; return;
} }
// Everything else: network only // Everything else: network only, don't intercept failures
event.respondWith(fetch(event.request)); // (Returning without calling event.respondWith lets the browser handle it natively)
}); });

View file

@ -182,7 +182,7 @@ module.exports = function setupDeviceSocket(io) {
if (device) { if (device) {
// Validate device token (skip for legacy devices that don't have a token yet) // Validate device token (skip for legacy devices that don't have a token yet)
if (device.device_token && !validateDeviceToken(device_id, device_token)) { 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' }); socket.emit('device:auth-error', { error: 'Invalid device token' });
return; return;
} }