fix(#4313): guard EventSource.close() calls to suppress ERR_ABORTED noise

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.
This commit is contained in:
bergeouss committed 2026-06-18 17:25:18 +00:00
1 parent bc11cfe6d2
commit d812f70116
4 files changed
+20 -20

No files matched your search

+9 -9
View File
@@ -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;
+2 -2
View File
@@ -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;
+3 -3
View File
@@ -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();
+6 -6
View File
@@ -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});