Fix iOS WebSocket hang with self-signed TLS certificates

iOS Safari silently rejects wss:// connections to servers using
self-signed certificates — the trust exception accepted for the page
does not extend to WebSocket handshakes. This causes the UI to hang
on "Establishing WebSocket connection..." indefinitely.

- Make --certificate and --key optional; server runs plain HTTP when omitted
- When TLS is enabled, also start a plain HTTP listener on --http-port
  (default 8081) so iOS clients can connect via ws:// instead of wss://
- Add a 5-second connection timeout on the frontend that detects the hang
  and shows a clickable link to the HTTP fallback URL

https://claude.ai/code/session_01VJ4CsBALnYcVhFJpY5cD5k
This commit is contained in:
Claude 2026-02-09 09:42:51 +00:00
parent bd3a5e2af0
commit c617a371cf
No known key found for this signature in database
2 changed files with 92 additions and 17 deletions

View file

@ -500,25 +500,44 @@
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
connectionStatus.textContent = 'Establishing WebSocket connection...';
state.ws = new WebSocket(wsUrl);
// iOS Safari silently rejects wss:// connections to servers with
// self-signed certificates — onopen never fires and the UI hangs.
// Use a timeout to detect this and show a helpful message.
const connectTimeout = setTimeout(() => {
if (state.ws && state.ws.readyState !== WebSocket.OPEN) {
console.warn('WebSocket connection timed out');
state.ws.close();
if (window.location.protocol === 'https:') {
const httpPort = parseInt(window.location.port || '443', 10) + 1;
const httpUrl = `http://${window.location.hostname}:${httpPort}`;
connectionStatus.innerHTML =
'Connection failed — iOS may not support self-signed WSS certificates.<br>' +
'Try the plain HTTP URL instead: <a href="' + httpUrl + '" style="color:#60a5fa;text-decoration:underline">' + httpUrl + '</a>';
}
}
}, 5000);
state.ws.onopen = () => {
clearTimeout(connectTimeout);
console.log('WebSocket connected');
state.reconnectAttempts = 0;
connectionStatus.textContent = 'Connected, waiting for welcome...';
};
state.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleSignal(msg);
};
state.ws.onclose = () => {
clearTimeout(connectTimeout);
console.log('WebSocket disconnected');
updateConnectionStatus(false);
if (state.reconnectAttempts < state.maxReconnectAttempts) {
state.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000);
@ -527,8 +546,9 @@
setTimeout(connect, delay);
}
};
state.ws.onerror = (err) => {
clearTimeout(connectTimeout);
console.error('WebSocket error:', err);
};
}