From 821f0a0a8f07da39314d65ce164c9efd93910b07 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Mon, 10 Aug 2026 04:53:41 +0000 Subject: [PATCH] Fix WebSocket reconnect: refused upgrades are 1006 not 1008, back off, prompt once --- static/app.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/static/app.js b/static/app.js index d3bce03..08c3f19 100644 --- a/static/app.js +++ b/static/app.js @@ -5,9 +5,17 @@ const { createApp, ref, computed, watch, onMounted, onUnmounted } = Vue; const TOKEN_KEY = 'chart-token'; let authToken = localStorage.getItem(TOKEN_KEY) || ''; +// Reconnects and the status poll both hit 401s, so without this a visitor +// who cancels gets asked again every couple of seconds. +let promptDeclined = false; + function promptForToken() { + if (promptDeclined) return false; const entered = window.prompt('Access token for this chart', ''); - if (entered === null) return false; + if (entered === null) { + promptDeclined = true; + return false; + } authToken = entered.trim(); localStorage.setItem(TOKEN_KEY, authToken); return true; @@ -55,6 +63,7 @@ createApp({ const now = ref(Date.now()); let chartApi = null; let socket = null; + let retryDelay = 2000; let timer = null; const barAge = computed(() => { @@ -76,6 +85,7 @@ createApp({ const query = authToken ? `?token=${encodeURIComponent(authToken)}` : ''; socket = new WebSocket(`${protocol}://${location.host}/ws${query}`); socket.onopen = () => { + retryDelay = 2000; socket.send(JSON.stringify({ type: 'subscribe', tf: timeframe.value })); sendPrefs(); }; @@ -102,11 +112,17 @@ createApp({ playAlert(); } }; - socket.onclose = event => { + socket.onclose = async () => { status.value.stream = 'disconnected'; - // 1008 is the server rejecting our token; ask once, then reconnect. - if (event.code === 1008 && !promptForToken()) return; - setTimeout(connect, 2000); + // A refused upgrade reaches the browser as 1006, not 1008: the server + // rejects the handshake with HTTP 403 before any close frame exists. + // So the close code cannot distinguish "bad token" from "server + // restarting". Re-check over HTTP instead — apiFetch prompts for a + // token when that is what is actually wrong — and back off so a + // visitor without one is not reconnecting twice a second forever. + await refreshStatus(); + retryDelay = Math.min(retryDelay * 2, 30000); + setTimeout(connect, retryDelay); }; }