diff --git a/extension/src/background.test.ts b/extension/src/background.test.ts index e45614843..84c61fab5 100644 --- a/extension/src/background.test.ts +++ b/extension/src/background.test.ts @@ -877,6 +877,30 @@ describe('background tab isolation', () => { expect(mod.__test__.getReconnectAttempts()).toBe(0); }); + it('pings without credentials and logs a non-OK status instead of swallowing it', async () => { + const { chrome } = createChromeMock(); + vi.stubGlobal('chrome', chrome); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const fetchMock = vi.fn(async () => ({ ok: false, status: 431 })); + vi.stubGlobal('fetch', fetchMock); + + await import('./background'); + + await vi.waitFor(() => { + expect(fetchMock).toHaveBeenCalled(); + }); + + // The ping must not attach the localhost cookie jar — that is what pushes + // the request past Node's header limit and makes the daemon answer 431. + expect(fetchMock.mock.calls[0][1]).toMatchObject({ credentials: 'omit' }); + // A non-OK ping must be logged, not silently swallowed. + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('HTTP 431')); + // The WebSocket must not be attempted after a failed ping. + expect(MockWebSocket.instances).toHaveLength(0); + + warnSpy.mockRestore(); + }); + it('ignores daemon commands delivered to a superseded WebSocket', async () => { const { chrome } = createChromeMock(); vi.stubGlobal('chrome', chrome); diff --git a/extension/src/background.ts b/extension/src/background.ts index 4918894c2..1bdbfc08e 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -138,14 +138,24 @@ async function connectAttempt(): Promise { if (isDaemonSocketActive()) return; try { - const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1000) }); + // omit credentials so the browser doesn't attach the localhost cookie jar — + // a large jar can push the request past Node's default header limit and make + // the daemon answer 431, silently wedging the connect loop forever. + const res = await fetch(DAEMON_PING_URL, { + signal: AbortSignal.timeout(1000), + credentials: 'omit', + }); if (!res.ok) { + console.warn(`[opencli] daemon ping failed: HTTP ${res.status}`); scheduleReconnect(); return; // unexpected response — not our daemon, but keep polling. } // Daemon is reachable — proceed straight to the WebSocket below. reconnectAttempts = 0; } catch { + // Daemon not running is the expected idle state — keep the probe silent to + // avoid per-poll service-worker noise (see connect() docstring). The 431 + // wedge this fixes is surfaced in the !res.ok branch above. scheduleReconnect(); return; // daemon not running — keep polling until the next daemon spawn. }