You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
V2 event streams can stall with Kaspersky without an SSE Accept header #48771
On Windows with Kaspersky 21.26 enabled, the V2 CLI can remain on Restarting service... for minutes even though /api/health responds normally. The generated Promise SSE client does not set Accept: text/event-stream, and the initial /api/event response can be delayed beyond the CLI's two-second connection deadline.
I compared the same authenticated requests against one running 2.0.3 backend, without restarting it or changing its configuration:
Kaspersky state
Unspecified / wildcard Accept (6 requests)
Explicit SSE Accept (3 requests)
Enabled
4 timed out at 2.5 s; 2 took 1.3-1.4 s
3-6 ms
Disabled for the comparison
All succeeded in 3-40 ms
3-5 ms
Protection restored
3 timed out at 2.5 s; 3 took 1.1-1.4 s
3-4 ms
The request-header-dependent delay also reproduces with a separate Node HTTP server/client, without OpenCode plugins. This identifies a compatibility problem with the enabled security software, not a particular internal Kaspersky module.
The EventSource specification says user agents may set this header, not that it is mandatory. Setting it by default for Promise SSE requests would improve interoperability; explicit caller headers should remain respected. SvelteKit has a matching Kaspersky report.
OpenCode version
Observed in 2.0.2 and 2.0.3. The default is still missing on v2 at 7a31b5c0f76ce1c06befacc09b47b4fc3c71c408, in packages/httpapi-codegen/src/index.ts and packages/client/src/promise/generated/client.ts.
Steps to reproduce
On an affected Windows/Kaspersky setup, start the V2 CLI and observe repeated connection timeouts while the backend remains healthy.
Compare authenticated streaming GET requests to the same /api/event endpoint with the default Accept and with Accept: text/event-stream.
The explicitly identified SSE request receives server.connected promptly; the default request can stall until later traffic arrives.
Related browser-side cause with the same symptom: in Firefox, /api/event responses carry Cache-Control: no-cache, which still lets Firefox store the stream in its HTTP cache. A second concurrent /api/event request (e.g. a second tab) then waits for the never-ending cache write and is never sent. Recommend serving event streams with Cache-Control: no-store (and/or fetching them with cache: "no-store") alongside the SSE Accept header. Details: #55369
Thanks for the detailed measurements. I reproduced this without Kaspersky. A small local proxy stood in for it: it holds back /api/event responses for 5 s unless the request carries Accept: text/event-stream, and it logs the Accept header of each request.
With opencode serve behind that proxy and the TUI started with opencode --server <proxy>, on 2.0.3:
Every /api/event request from the CLI was sent with Accept: */*.
Each attempt was aborted at the 2 s deadline (Timed out connecting to server) and retried about every 3 s, while the TUI showed "Connection lost / Reconnecting".
/api/health and a curl request to /api/event with Accept: text/event-stream went through the same proxy and got server.connected straight away.
The same thing happens on the latest dev (ecf1d36e7), run from source. No default SSE Accept header has been added to the Promise client or its generator since.
The script below reproduces it (proxy included). There is no recording, because our TUI test harness can't be routed through a proxy; the results come from the TUI's own output. Related: #55741 (same timeout loop, traced to the same Kaspersky behaviour), #53498 (the 2 s connect deadline behind slow proxies), and the closed PR #48779.
Reproduction script (repro.sh)
#!/usr/bin/env bash# Reproduces #48771 without Kaspersky: a local proxy stands in for the security software and# holds back /api/event responses for 5 s unless the request has `Accept: text/event-stream`.# Usage: OC="opencode" ./repro.sh (or OC="bun --conditions=browser --preload=... packages/cli/src/index.ts")set -u
OC=${OC:-opencode}
DIR=$(cd "$(dirname "$0")"&& pwd)
mkdir -p "$DIR/proj"&&cd"$DIR/proj"
cat >"$DIR/proxy.mjs"<<'JS'// Simulates the reported security-software behaviour: /api/event responses are held back// for DELAY ms unless the request carries `Accept: text/event-stream`. Logs each request's Accept.import http from "node:http"const [listen, target, delay] = [Number(process.argv[2]), Number(process.argv[3]), Number(process.argv[4] ?? 5000)]http.createServer((req, res) => { const sse = (req.headers.accept ?? "").includes("text/event-stream") if (req.url.startsWith("/api/event")) console.log(new Date().toISOString(), req.method, req.url, "accept=", JSON.stringify(req.headers.accept ?? null), sse ? "pass-through" : `held ${delay}ms`) const up = http.request({ host: "127.0.0.1", port: target, path: req.url, method: req.method, headers: req.headers }, (r) => { const go = () => { if (res.destroyed) return; res.writeHead(r.statusCode, r.headers); r.pipe(res) } if (req.url.startsWith("/api/event") && !sse) setTimeout(go, delay); else go() }) up.on("error", () => res.destroy()); res.on("close", () => up.destroy()) req.pipe(up)}).listen(listen, "127.0.0.1", () => console.log("proxy", listen, "->", target))JSexport OPENCODE_SERVER_PASSWORD=repro-password
$OC serve --hostname 127.0.0.1 --port 4601 > /tmp/serve.log 2>&1& SERVE=$!
node "$DIR/proxy.mjs" 4602 4601 5000 > /tmp/proxy.log 2>&1& PROXY=$!
sleep 5
echo"health via proxy: $(curl -s -m 3 -u opencode:$OPENCODE_SERVER_PASSWORD http://127.0.0.1:4602/api/health)"echo"/api/event, SSE Accept -> first bytes:"
curl -s -N -m 2 -u opencode:$OPENCODE_SERVER_PASSWORD -H 'Accept: text/event-stream' http://127.0.0.1:4602/api/event | head -c 200;echo# Run the TUI against the proxy for 20 s in a pseudo-terminal.
timeout 20 script -qfc "$OC --server http://127.0.0.1:4602 --log-level debug --print-logs" /tmp/tui.txt >/dev/null 2>&1echo"--- proxy log (Accept header the CLI sent on /api/event) ---"; cat /tmp/proxy.log
echo"--- TUI/log messages ---"
sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g' /tmp/tui.txt | grep -a -o -i "timed out connecting to server\|restarting service\|connection lost\|reconnecting"| sort | uniq -c
kill$SERVE$PROXY2>/dev/null
Activity
LeonMueller-OneAndOnly commentedon Oct 14, 2026
Related browser-side cause with the same symptom: in Firefox,
/api/eventresponses carryCache-Control: no-cache, which still lets Firefox store the stream in its HTTP cache. A second concurrent/api/eventrequest (e.g. a second tab) then waits for the never-ending cache write and is never sent. Recommend serving event streams withCache-Control: no-store(and/or fetching them withcache: "no-store") alongside the SSEAcceptheader. Details: #55369Upitep commentedon Oct 26, 2026
I can confirm that PR #48779 fixes this issue on Windows with Kaspersky Endpoint Security enabled.
After upgrading from OpenCode 2.0.6 to 2.1.4, both CLI/TUI and Desktop became practically unusable due to repeated SSE disconnections.
We rebuilt both CLI and Desktop 2.1.4 with the patch from #48779 and verified:
No Kaspersky exclusions or security policy changes were required.
Unfortunately, PR #48779 was closed by automated cleanup rather than technical review.
Could a maintainer please reconsider reopening or merging the fix, or porting it into the current release?
This is a reproducible issue with a tested, working solution. It would be unfortunate to lose the fix because of automated PR cleanup.
Thanks to @DEAN-Cherry for the patch!
opencodejr commentedon Oct 28, 2026
Thanks for the detailed measurements. I reproduced this without Kaspersky. A small local proxy stood in for it: it holds back
/api/eventresponses for 5 s unless the request carriesAccept: text/event-stream, and it logs the Accept header of each request.With
opencode servebehind that proxy and the TUI started withopencode --server <proxy>, on 2.0.3:/api/eventrequest from the CLI was sent withAccept: */*.Timed out connecting to server) and retried about every 3 s, while the TUI showed "Connection lost / Reconnecting"./api/healthand acurlrequest to/api/eventwithAccept: text/event-streamwent through the same proxy and gotserver.connectedstraight away.The same thing happens on the latest
dev(ecf1d36e7), run from source. No default SSE Accept header has been added to the Promise client or its generator since.The script below reproduces it (proxy included). There is no recording, because our TUI test harness can't be routed through a proxy; the results come from the TUI's own output. Related: #55741 (same timeout loop, traced to the same Kaspersky behaviour), #53498 (the 2 s connect deadline behind slow proxies), and the closed PR #48779.
Reproduction script (repro.sh)
Run it:
bash repro.sh