From d812f701165d576ec9d2c5cbaed5fe41c739ab15 Mon Sep 17 00:00:00 2001 From: bergeouss Date: Thu, 18 Jun 2026 17:25:18 +0000 Subject: [PATCH] fix(#4313): guard EventSource.close() calls to suppress ERR_ABORTED noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add readyState !== CLOSED (2) guards to all EventSource.close() calls across messages.js, sessions.js, panels.js, and terminal.js. Chromium reports net::ERR_ABORTED when close() is called on an already-closed EventSource, polluting the console during normal lifecycle transitions (session switch, panel close, stream teardown). Key hotspots: - closeLiveStream() double-close via onerror → _closeSource chain - _closeSessionEventsSSE, _gatewaySSE.onerror missing try/catch - _kanbanStopPolling, terminal close handlers 20 close() calls guarded across 4 files, no structural changes to stream lifecycle. --- static/messages.js | 18 +++++++++--------- static/panels.js | 4 ++-- static/sessions.js | 6 +++--- static/terminal.js | 12 ++++++------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/static/messages.js b/static/messages.js index 54d67793..b5b7a7de 100644 --- a/static/messages.js +++ b/static/messages.js @@ -1418,7 +1418,7 @@ function closeLiveStream(sessionId, streamId, source){ // reattach path will rebuild it from INFLIGHT/server state if the user returns. if(typeof _clearLiveRunStatusTimer==='function') _clearLiveRunStatusTimer(sessionId); if(typeof hideLiveRunStatus==='function') hideLiveRunStatus(sessionId); - try{live.source.close();}catch(_){ } + try{if(live.source&&live.source.readyState!==2)live.source.close();}catch(_){ } delete LIVE_STREAMS[sessionId]; // closeLiveStream() is called during session-switch teardown for any session // the user is no longer viewing. The stream is still active on the server, @@ -2924,7 +2924,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ function _wireSSE(source){ const existingLive=LIVE_STREAMS[activeSid]; if(existingLive&&existingLive.source&&existingLive.source!==source){ - try{existingLive.source.close();}catch(_){ } + try{if(existingLive.source.readyState!==2)existingLive.source.close();}catch(_){ } } LIVE_STREAMS[activeSid]={streamId,source}; @@ -3672,7 +3672,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ if(typeof finalizeThinkingCard==='function') finalizeThinkingCard(); // Application-level error sent explicitly by the server (rate limit, crash, etc.) // This is distinct from the SSE network 'error' event below. - source.close(); + try{if(source&&source.readyState!==2)source.close();}catch(_){ } _clearOwnerInflightState(); _clearApprovalForOwner(); _clearClarifyForOwner('terminal'); @@ -3782,7 +3782,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ return; } if(typeof recordClientSSEError==='function') recordClientSSEError('chat-response',{ready_state:source?source.readyState:null,session_id:activeSid,stream_id:streamId,reason:'chat EventSource.onerror'}); - source.close(); + try{if(source&&source.readyState!==2)source.close();}catch(_){ } if(_deferStreamErrorIfOffline()) return; if(_deferStreamErrorIfPageHidden(source)) return; _closeSource(source); @@ -3840,7 +3840,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ _streamFadeCleanupReduceMotionListener(); _smdEndParser(); if(typeof finalizeThinkingCard==='function') finalizeThinkingCard(); - source.close(); + try{if(source&&source.readyState!==2)source.close();}catch(_){ } _clearOwnerInflightState(); _clearApprovalForOwner(); _clearClarifyForOwner('cancelled'); @@ -4445,7 +4445,7 @@ function stopApprovalPollingForSession(sid) { function stopApprovalPolling() { if (_approvalPollTimer) { clearInterval(_approvalPollTimer); _approvalPollTimer = null; } - if (_approvalEventSource) { try { _approvalEventSource.close(); } catch(_){} _approvalEventSource = null; } + if (_approvalEventSource) { try { if(_approvalEventSource.readyState!==2)_approvalEventSource.close(); } catch(_){} _approvalEventSource = null; } if (_approvalSSEHealthTimer) { clearInterval(_approvalSSEHealthTimer); _approvalSSEHealthTimer = null; } _approvalFallbackPollInFlight = false; _approvalPollingSessionId = null; @@ -4581,7 +4581,7 @@ function startSessionStream(sid) { // it in the interim (stale onerror from a superseded stream), in // which case we must not stomp the live one. if (_sessionEventSource === es) { - try { es.close(); } catch (_) {} + try { if(es.readyState!==2)es.close(); } catch (_) {} _sessionEventSource = null; } _sessionStreamReconnectTimer = setTimeout(() => { @@ -4600,7 +4600,7 @@ function startSessionStream(sid) { function stopSessionStream() { if (_sessionStreamReconnectTimer) { clearTimeout(_sessionStreamReconnectTimer); _sessionStreamReconnectTimer = null; } if (_sessionEventSource) { - try { _sessionEventSource.close(); } catch(_){} + try { if(_sessionEventSource.readyState!==2)_sessionEventSource.close(); } catch(_){} _sessionEventSource = null; } _sessionStreamSessionId = null; @@ -5212,7 +5212,7 @@ function stopClarifyPollingForSession(sid) { } function stopClarifyPolling() { - if (_clarifyEventSource) { try { _clarifyEventSource.close(); } catch(_){} _clarifyEventSource = null; } + if (_clarifyEventSource) { try { if(_clarifyEventSource.readyState!==2)_clarifyEventSource.close(); } catch(_){} _clarifyEventSource = null; } if (_clarifyFallbackTimer) { clearInterval(_clarifyFallbackTimer); _clarifyFallbackTimer = null; } if (_clarifyHealthTimer) { clearInterval(_clarifyHealthTimer); _clarifyHealthTimer = null; } _clarifyFallbackPollInFlight = false; diff --git a/static/panels.js b/static/panels.js index d7e87bca..967ba285 100644 --- a/static/panels.js +++ b/static/panels.js @@ -2044,13 +2044,13 @@ function _kanbanStartPolling(){ function _kanbanStopPolling(){ if (_kanbanPollTimer) { clearInterval(_kanbanPollTimer); _kanbanPollTimer = null; } - if (_kanbanEventSource) { try { _kanbanEventSource.close(); } catch(_) {} _kanbanEventSource = null; } + if (_kanbanEventSource) { try { if(_kanbanEventSource.readyState!==2)_kanbanEventSource.close(); } catch(_) {} _kanbanEventSource = null; } } function _kanbanStartEventStream(){ // Tear down any prior stream before opening a new one (board switch, // login change, etc.). - if (_kanbanEventSource) { try { _kanbanEventSource.close(); } catch(_) {} _kanbanEventSource = null; } + if (_kanbanEventSource) { try { if(_kanbanEventSource.readyState!==2)_kanbanEventSource.close(); } catch(_) {} _kanbanEventSource = null; } const since = Number(_kanbanLatestEventId || 0); let url = '/api/kanban/events/stream' + _kanbanBoardQuery({since: since}); let es; diff --git a/static/sessions.js b/static/sessions.js index d8c8afda..95afc717 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -3932,7 +3932,7 @@ function _installSidebarSseFocusHook(){ function _closeSessionEventsSSE(){ if(_sessionEventsSSE){ - _sessionEventsSSE.close(); + try{if(_sessionEventsSSE.readyState!==2)_sessionEventsSSE.close();}catch(_){ } _sessionEventsSSE = null; } } @@ -4143,7 +4143,7 @@ function startGatewaySSE(){ _gatewaySSE.onerror = () => { if(typeof recordClientSSEError==='function') recordClientSSEError('gateway-sessions',{ready_state:_gatewaySSE?_gatewaySSE.readyState:null,reason:'gateway EventSource.onerror'}); if(_gatewaySSE){ - _gatewaySSE.close(); + try{if(_gatewaySSE.readyState!==2)_gatewaySSE.close();}catch(_){ } _gatewaySSE = null; } void probeGatewaySSEStatus(); @@ -4155,7 +4155,7 @@ function startGatewaySSE(){ function stopGatewaySSE(){ if(_gatewaySSE){ - _gatewaySSE.close(); + try{if(_gatewaySSE.readyState!==2)_gatewaySSE.close();}catch(_){ } _gatewaySSE = null; } stopGatewayPollFallback(); diff --git a/static/terminal.js b/static/terminal.js index 5f46d650..69002012 100644 --- a/static/terminal.js +++ b/static/terminal.js @@ -401,7 +401,7 @@ function _connectTerminalOutput(){ const sid=_terminalSessionId(); if(!sid)return; if(TERMINAL_UI.source){ - try{TERMINAL_UI.source.close();}catch(_){} + try{if(TERMINAL_UI.source.readyState!==2)TERMINAL_UI.source.close();}catch(_){} TERMINAL_UI.source=null; } const url=new URL('api/terminal/output',document.baseURI||location.href); @@ -419,7 +419,7 @@ function _connectTerminalOutput(){ source.addEventListener('terminal_closed',()=>{ if(TERMINAL_UI.source!==source)return; if(TERMINAL_UI.term)TERMINAL_UI.term.writeln('\r\n[terminal closed]\r\n'); - try{source.close();}catch(_){} + try{if(source&&source.readyState!==2)source.close();}catch(_){} TERMINAL_UI.source=null; setTimeout(()=>closeComposerTerminal(null,{skipApi:true}),260); }); @@ -428,7 +428,7 @@ function _connectTerminalOutput(){ let msg=t('terminal_error'); try{msg=(JSON.parse(ev.data)||{}).error||msg;}catch(_){} if(TERMINAL_UI.term)TERMINAL_UI.term.writeln('\r\n[terminal error] '+msg+'\r\n'); - try{source.close();}catch(_){} + try{if(source&&source.readyState!==2)source.close();}catch(_){} TERMINAL_UI.source=null; }); } @@ -557,7 +557,7 @@ async function closeComposerTerminal(sessionId,opts){ opts=opts||{}; const sid=sessionId||TERMINAL_UI.sessionId||_terminalSessionId(); if(TERMINAL_UI.source){ - try{TERMINAL_UI.source.close();}catch(_){} + try{if(TERMINAL_UI.source&&TERMINAL_UI.source.readyState!==2)TERMINAL_UI.source.close();}catch(_){} TERMINAL_UI.source=null; } if(sid&&!opts.skipApi){ @@ -588,7 +588,7 @@ async function closeComposerTerminal(sessionId,opts){ async function restartComposerTerminal(){ if(!TERMINAL_UI.open||TERMINAL_UI.collapsed)return; if(TERMINAL_UI.source){ - try{TERMINAL_UI.source.close();}catch(_){} + try{if(TERMINAL_UI.source&&TERMINAL_UI.source.readyState!==2)TERMINAL_UI.source.close();}catch(_){} TERMINAL_UI.source=null; } if(TERMINAL_UI.term)TERMINAL_UI.term.reset(); @@ -646,7 +646,7 @@ async function _resizeComposerTerminal(){ } window.addEventListener('beforeunload',()=>{ - if(TERMINAL_UI.source)try{TERMINAL_UI.source.close();}catch(_){} + if(TERMINAL_UI.source)try{if(TERMINAL_UI.source&&TERMINAL_UI.source.readyState!==2)TERMINAL_UI.source.close();}catch(_){} if(TERMINAL_UI.sessionId){ const url=new URL('api/terminal/close',document.baseURI||location.href).href; const body=JSON.stringify({session_id:TERMINAL_UI.sessionId});