Safari: revert websocket redirect workaround (#28753)

This commit is contained in:
Michael Geers 2026-04-02 17:50:27 +02:00 • committed by GitHub
parent 0e36680ec9
commit 196c56fab7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1 additions and 61 deletions

View file

@ -33,9 +33,6 @@ import HelpModal from "../components/HelpModal.vue";
import collector from "../mixins/collector";
import { defineComponent } from "vue";
const WS_OPEN_TIMEOUT_MS = 5000;
const WS_RETRY_PARAM = "wsRetry";
// assume offline if not data received for 5 minutes
let lastDataReceived = new Date();
const maxDataAge = 60 * 1000 * 5;
@ -67,7 +64,6 @@ export default defineComponent({
data: () => {
return {
reconnectTimeout: null as number | null,
openTimeout: null as number | null,
ws: null as WebSocket | null,
authNotConfigured: false,
};
@ -150,34 +146,6 @@ export default defineComponent({
window.clearTimeout(this.reconnectTimeout);
}
},
// Safari 26 bug: with hash fragment URLs the HTTP upgrade
// request is sometimes silently dropped when serving from cache.
// Recover by navigating without hash, once (wsRetry guards against loops).
startOpenTimeout() {
const url = new URL(window.location.href);
if (url.searchParams.has(WS_RETRY_PARAM)) return;
this.openTimeout = window.setTimeout(() => {
console.warn("websocket open timeout, forcing navigation");
this.ws?.close();
url.hash = "";
url.searchParams.set(WS_RETRY_PARAM, "true");
window.location.href = url.href;
}, WS_OPEN_TIMEOUT_MS);
},
clearOpenTimeout(success = false) {
if (this.openTimeout) {
window.clearTimeout(this.openTimeout);
this.openTimeout = null;
}
if (success) {
const url = new URL(window.location.href);
if (url.searchParams.has(WS_RETRY_PARAM)) {
console.warn("websocket open timeout recovered, clearing retry param");
url.searchParams.delete(WS_RETRY_PARAM);
window.history.replaceState(window.history.state, "", url.href);
}
}
},
pageShowHandler(event: PageTransitionEvent) {
if (event.persisted) {
this.clearReconnectTimeout();
@ -201,7 +169,6 @@ export default defineComponent({
}, 2500);
},
disconnect() {
this.clearOpenTimeout();
if (this.ws) {
this.ws.onerror = null;
this.ws.onopen = null;
@ -228,22 +195,17 @@ export default defineComponent({
const loc = new URL("ws", window.location.href);
loc.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
this.ws = new WebSocket(loc.href);
this.startOpenTimeout();
this.ws.onerror = () => {
console.log({ message: "Websocket error. Trying to reconnect." });
this.clearOpenTimeout();
this.ws?.close();
};
this.ws.onopen = () => {
this.clearOpenTimeout(true);
console.log("websocket connected");
window.app.setOnline();
};
this.ws.onclose = () => {
this.clearOpenTimeout();
window.app.setOffline();
this.reconnect();
};

View file

@ -43,25 +43,3 @@ test("show offline when websocket is closed", async ({ page }) => {
await expect(page.getByText("Not connected to a server.")).toBeVisible();
await expect(page.getByRole("link", { name: "Let's start configuration" })).toBeHidden();
});
test("force navigation after websocket open timeout", async ({ page }) => {
// Replace WebSocket with a mock that never fires onopen,
// simulating Safari's bug where the upgrade request is silently dropped.
await page.addInitScript(() => {
(window as any).WebSocket = class {
readyState = 0;
onopen: (() => void) | null = null;
onclose: ((ev: CloseEvent) => void) | null = null;
onerror: (() => void) | null = null;
onmessage: (() => void) | null = null;
close() {
this.readyState = 3;
this.onclose?.(new CloseEvent("close"));
}
send() {}
};
});
await page.goto("/");
// after the 5s open timeout the app navigates to strip the hash fragment
await page.waitForURL(/wsRetry/, { timeout: 10000 });
});