Description
When opencode web serves the embedded web UI, responses carry no cache directives at all — no Cache-Control, no ETag, no Last-Modified:
// packages/opencode/src/server/shared/ui.ts (~L55-62)
function embeddedUIResponse(file: string, body: Uint8Array) {
const mime = FSUtil.mimeType(file)
const headers = new Headers({ "content-type": mime })
if (mime.startsWith("text/html")) {
headers.set("content-security-policy", cspForHtml(new TextDecoder().decode(body)))
}
return HttpServerResponse.raw(body, { headers })
}
With no explicit policy, cache behavior is left to browser heuristics and any intermediary (reverse proxy, CDN, Cloudflare Tunnel). HTML, the manifest, and entry JS can be served stale after a deploy while the user has no signal that the client is outdated — a different mechanism than the event-stream stall (#39030) but the same user-facing symptom: "the page only shows the new version after Ctrl+F5".
Expected behavior
Explicit, intentional cache policy:
const headers = new Headers({ "content-type": mime })
if (mime.startsWith("text/html")) {
// HTML is the version pivot: always revalidate
headers.set("cache-control", "no-cache")
headers.set("content-security-policy", cspForHtml(new TextDecoder().decode(body)))
} else if (/\.[0-9a-z]{8,}\./.test(file) || file.includes("/assets/")) {
// content-addressed bundle assets only
headers.set("cache-control", "public, max-age=31536000, immutable")
} else {
// manifest, icons, fonts: revalidate cheaply
headers.set("cache-control", "no-cache")
headers.set("etag", etagFor(body)) // cheap content hash
}
If assets are content-hashed by the bundler, immutable is safe only for those files; everything else should revalidate. Bonus: emit the server/app version in a header (e.g. X-OpenCode-Version) so a client older than the server can offer a one-click safe reload (preserving prompt drafts).
Steps to reproduce
opencode web, open in a browser.
- Upgrade the server and redeploy while the tab is open.
- The open tab keeps running the old UI; depending on intermediary caching, even a normal reload can keep serving stale HTML until a hard refresh.
OpenCode version
v1.18.33 (dev @ 083ed266e)
Related work
| Ref |
State |
What it reports / fixes |
| #41280 |
open issue |
Bundled web UI version mismatch (package 1.18.15 serves UI 1.18.14) — adjacent version-coherence problem; an explicit X-OpenCode-Version header would let the client detect it. |
| #50398 |
open issue |
Web UI deployment "reset everything" — deploy-time staleness/state inconsistencies. |
| #48622 |
open issue |
Request for background auto-update — the version-mismatch affordance proposed here is its UI half. |
| #51860 |
open issue |
Connection-health surface (filed alongside this) — consumes the version header for its "client outdated" state. |
| #51857 |
open issue |
SSE stall watchdog regression (filed alongside this) — the other half of "page only updates after Ctrl+F5": this one is deploy-staleness, that one is live-update-staleness. |
Triage note for maintainers
Low-effort, high-leverage hardening: a handful of headers on one response path. Today nothing explicitly says "this HTML is the version pivot — revalidate it", so correctness depends on every intermediary in between (nginx, Cloudflare Tunnel, corporate proxies, AV scanners) doing the right thing by default. Worth batching with #51857 so both staleness mechanisms get fixed in the same release.
Description
When
opencode webserves the embedded web UI, responses carry no cache directives at all — noCache-Control, noETag, noLast-Modified:With no explicit policy, cache behavior is left to browser heuristics and any intermediary (reverse proxy, CDN, Cloudflare Tunnel). HTML, the manifest, and entry JS can be served stale after a deploy while the user has no signal that the client is outdated — a different mechanism than the event-stream stall (#39030) but the same user-facing symptom: "the page only shows the new version after
Ctrl+F5".Expected behavior
Explicit, intentional cache policy:
If assets are content-hashed by the bundler,
immutableis safe only for those files; everything else should revalidate. Bonus: emit the server/app version in a header (e.g.X-OpenCode-Version) so a client older than the server can offer a one-click safe reload (preserving prompt drafts).Steps to reproduce
opencode web, open in a browser.OpenCode version
v1.18.33 (
dev@083ed266e)Related work
X-OpenCode-Versionheader would let the client detect it.Triage note for maintainers
Low-effort, high-leverage hardening: a handful of headers on one response path. Today nothing explicitly says "this HTML is the version pivot — revalidate it", so correctness depends on every intermediary in between (nginx, Cloudflare Tunnel, corporate proxies, AV scanners) doing the right thing by default. Worth batching with #51857 so both staleness mechanisms get fixed in the same release.