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
web: second Firefox tab never connects to /api/event - Cache-Control: no-cache lets Firefox's HTTP cache block concurrent SSE requests #55369
In Firefox (also Firefox-based browsers such as Zen), the web UI's live event stream hangs as soon as the same opencode server is open in a second tab. The banner/loading state never clears until the other tab is closed.
GET /api/event is answered with Cache-Control: no-cache, no-transform. no-cache still allows storing, so Firefox writes the response into its HTTP cache. A concurrent request for the same URL then waits for that cache entry to finish being written - which never happens for an endless SSE stream. The second tab's /api/event requests are queued inside the browser forever:
DevTools shows them with no status, 0 B and "No timings for request" - they are never sent.
The server never sees them (nothing in the opencode log, no connection attempt).
The web client aborts each attempt after its 2 s connect deadline and retries, so the queue just grows.
Other requests to the same host at the same moment (e.g. /api/session/active) succeed instantly over the same HTTP/2 connection, so this is not a connection or stream limit - only the repeated /api/event URL is blocked.
Closing the other tab makes the stream connect immediately.
Server responses confirmed from opencode serve v2.0.23:
HTTP/1.1 200 OK
cache-control: no-cache, no-transform
x-accel-buffering: no
content-type: text/event-stream
Transfer-Encoding: chunked
Suggested fix
Either (or both):
Server: send Cache-Control: no-store (e.g. no-store, no-transform) for /api/event (and other SSE endpoints) in packages/opencode/src/server/routes/instance/httpapi/handlers/event.ts. Event streams should never be stored.
Client: issue the event-stream fetch with cache: "no-store", which bypasses the HTTP cache entirely.
We worked around it by wrapping window.fetch in the web UI and re-issuing /api/event as new Request(request, { headers, cache: "no-store" }).
Steps to reproduce
opencode serve, open the web UI in Firefox.
Open the same server/project in a second Firefox tab.
The second tab never receives live events; DevTools > Network shows /api/event requests piling up without status or timings.
Close the first tab - the second connects immediately.
Related
#48771, #53498, #51857 describe stalled/never-connecting event streams with other causes; this is a further, browser-side cause with the same symptom.
Thanks for the detailed report! I could reproduce this.
On 2.0.23 I ran opencode serve and opened the web UI in two tabs of the same Firefox profile (Playwright's Firefox):
The first tab's /api/event got 200 with cache-control: no-cache, no-transform.
In 15 seconds the second tab sent 5 /api/event requests and got no response to any of them.
A plain fetch("/api/event") from the second tab hung. The same fetch with cache: "no-store" got 200 immediately.
After I closed the first tab, the second tab connected right away.
It still happens on the latest dev (34d8ded). The server still sends Cache-Control: no-cache, no-transform for the event stream, and the client doesn't set cache: "no-store". There, two Firefox tabs fetching /api/event showed the same result: the second hangs until the first closes, unless it uses no-store.
The script below reproduces it with the web UI. Until there's a fix, your workaround of forcing cache: "no-store" on /api/event is the way around it. This may also contribute to the symptoms in #53498, #51857 and #48771, although each of those has its own cause.
Reproduction script (firefox-two-tabs.mjs)
// Reproduces #55369: in Firefox, a second tab on the same opencode server never// gets a response for GET /api/event while the first tab's stream is open.// Usage: OC_URL=http://127.0.0.1:4096 OC_PASSWORD=<server password> node firefox-two-tabs.mjs// Requires: npm i playwright && npx playwright install --with-deps firefoximport{firefox,chromium}from"playwright"import{createHmac}from"node:crypto"consturl=process.env.OC_URL??"http://127.0.0.1:4096"constpassword=process.env.OC_PASSWORDconstengine=process.env.BROWSER==="chromium" ? chromium : firefoxconstbrowser=awaitengine.launch()constcontext=awaitbrowser.newContext()// Authenticate like a paired browser: a signed session cookie (see packages/server/src/auth.ts).if(password){constexpires=String(Math.floor(Date.now()/1000)+3600)constkey=createHmac("sha256",password).update("opencode-session-v1").digest()consttoken=`${expires}.${createHmac("sha256",key).update(expires).digest("base64url")}`constu=newURL(url)awaitcontext.addCookies([{name: `opencode_session_${u.port}`,value: token, url }])}functiontrack(page,name){conststats={ name,requests: 0,responses: 0,statuses: []}page.on("request",(r)=>r.url().includes("/api/event")&&stats.requests++)page.on("response",(r)=>{if(!r.url().includes("/api/event"))returnstats.responses++stats.statuses.push(r.status()+" "+(r.headers()["cache-control"]??""))})returnstats}consttab1=awaitcontext.newPage()consts1=track(tab1,"tab1")awaittab1.goto(url)awaittab1.waitForTimeout(8000)consttab2=awaitcontext.newPage()consts2=track(tab2,"tab2")awaittab2.goto(url)awaittab2.waitForTimeout(15000)console.log("with tab1 open:",JSON.stringify(s1),JSON.stringify(s2))// Direct probe from tab2: does a plain fetch of /api/event get response headers?constprobe=async(cache)=>tab2.evaluate(async(cache)=>{constac=newAbortController()constt=setTimeout(()=>ac.abort(),5000)try{constr=awaitfetch("/api/event",{signal: ac.signal, cache })ac.abort()return"status "+r.status}catch(e){return"no response within 5s ("+e.name+")"}finally{clearTimeout(t)}},cache)console.log("tab2 fetch(/api/event) default cache:",awaitprobe("default"))console.log("tab2 fetch(/api/event) cache:no-store:",awaitprobe("no-store"))awaittab1.close()constbefore=s2.responsesawaittab2.waitForTimeout(8000)console.log("after closing tab1: tab2 new /api/event responses =",s2.responses-before)awaitbrowser.close()
Thanks for the detailed report and reproduction steps! I’d like to work on this. I’ll update the SSE cache handling to use no-store and verify that both Firefox tabs can connect before opening a PR.
Activity
opencode-agent commentedon Oct 14, 2026
Thanks for the detailed report! I could reproduce this.
On 2.0.23 I ran
opencode serveand opened the web UI in two tabs of the same Firefox profile (Playwright's Firefox):/api/eventgot200withcache-control: no-cache, no-transform./api/eventrequests and got no response to any of them.fetch("/api/event")from the second tab hung. The same fetch withcache: "no-store"got200immediately.It still happens on the latest
dev(34d8ded). The server still sendsCache-Control: no-cache, no-transformfor the event stream, and the client doesn't setcache: "no-store". There, two Firefox tabs fetching/api/eventshowed the same result: the second hangs until the first closes, unless it usesno-store.The script below reproduces it with the web UI. Until there's a fix, your workaround of forcing
cache: "no-store"on/api/eventis the way around it. This may also contribute to the symptoms in #53498, #51857 and #48771, although each of those has its own cause.Reproduction script (firefox-two-tabs.mjs)
Run it:
bun firefox-two-tabs.mjszonemeen commentedon Oct 14, 2026
Thanks for the detailed report and reproduction steps! I’d like to work on this. I’ll update the SSE cache handling to use no-store and verify that both Firefox tabs can connect before opening a PR.