Skip to content

[FE-1026] Cache immutable self-hosted UI assets - #4011

Merged
ardiewen merged 4 commits into
mainfrom
ardiewen/fe-1026-cache-self-hosted-assets
Oct 20, 2026
Merged

ardiewen merged 4 commits into
mainfrom
ardiewen/fe-1026-cache-self-hosted-assets

Conversation

@ardiewen

@ardiewen ardiewen commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

https://linear.app/temporal/issue/FE-1026/investigate-multiple-js-files-downloading

Summary

Self-hosted Temporal UI was serving SvelteKit's content-addressed /_app/immutable/* assets from the Go ui-server without cache headers, causing browsers to transfer the same JavaScript again on reload. The Go static handler now sends Cache-Control: public, max-age=31536000, immutable for successful immutable file responses.

This PR supersedes the delivery mechanism introduced by #3128. That PR established the correct cache policy in hooks.server.ts, but this repository builds with @sveltejs/adapter-static: exported files do not execute SvelteKit server hooks when the Go ui-server serves them. This PR removes that unreachable hook mutation and its dedicated tests while preserving the hook's resolve pass-through and UUID polyfill, then applies the same policy at the serving layer that owns the response.

Delivery-path findings

  • Self-hosted UI: the Go ui-server owns response headers for the embedded adapter-static build, so this is where the cache policy must be applied.
  • SvelteKit adapter-node: its native implementation likewise assigns the one-year immutable policy in the server that serves known build assets, rather than in an application hook. It serves only recorded files, keeps redirects and missing paths out of the immutable policy, and applies the policy to successful full and range responses.
  • Vercel: installed @sveltejs/adapter-vercel 6.3.4 generates its own /_app/immutable/ one-year cache rule and a no-store rule for missing immutable assets. Cloud UI therefore does not depend on this hook.
  • SvelteKit preview: the preview server applies its own immutable-asset cache header.
  • Raw adapter-static output: it contains files, not a serving policy. A custom external server or CDN remains responsible for its own headers.

Safety boundaries

  • Only successful, non-directory /_app/immutable/ file responses receive the public immutable directive.
  • 200 and 206 immutable file responses are cacheable; 301, 404, and 416 responses are not.
  • The global CSRF middleware can attach a per-client _csrf cookie and Vary: Cookie before the static handler writes its response. Cacheable immutable responses therefore remove Set-Cookie and only the Cookie token from Vary, while preserving unrelated tokens such as Gzip's Accept-Encoding.
  • HTML, non-immutable assets, directory listings, redirects, errors, and their existing cookie behavior remain unchanged.

Non-goals

SvelteKit adapter-node also supports HEAD, content-hash ETags, conditional 304 responses, and precompressed asset variants. Those are independent serving optimizations and are not required to correct the missing cache policy, so this PR does not add them to ui-server.

Verification

  • Reproduced a cookie-less immutable request through the real Gzip → CSRF → static-handler chain returning Vary: Accept-Encoding, Cookie before the fix.
  • Added a regression proving the cacheable response has no Set-Cookie, no Vary: Cookie, and still has Vary: Accept-Encoding.
  • Added coverage for successful files, 206 ranges, immutable directories, redirects, missing files, 416 ranges, and non-immutable assets.
  • go vet and go test ./server/... passed.
  • pnpm lint and pnpm check passed with existing warnings only.
  • pnpm test -- --run passed: 284 files, 3,711 tests, 2 skipped.
  • pnpm build:server passed.
  • Manual production-build self-hosted browser validation passed.

@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
holocene Ready Ready Preview Oct 13, 2026 5:35pm UTC

Request Review

@ardiewen
ardiewen marked this pull request as ready for review October 13, 2026 18:02
@ardiewen
ardiewen requested a review from a team as a code owner October 13, 2026 18:02
Comment thread server/server/route/ui.go
handler := http.FileServer(http.FS(assets))
return echo.WrapHandler(handler)
return echo.WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/_app/immutable/") && !strings.HasSuffix(r.URL.Path, "/") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does r.URL.Path take into account publicPath if they set that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I just tested this myself with publicPath set and it does work.

Image

Tracing it:

  • server/server/route/public_path.go is the middleware responsible for removing the prefix
  • runs before routing via e.Pre(route.PublicPath(cfg.PublicPath))

@Alex-Tideman
Alex-Tideman self-requested a review October 20, 2026 18:04
@ardiewen
ardiewen merged commit 4501519 into main Oct 20, 2026
30 checks passed
@ardiewen
ardiewen deleted the ardiewen/fe-1026-cache-self-hosted-assets branch October 20, 2026 18:08
Comment thread server/server/route/ui.go
func (w *immutableAssetResponseWriter) WriteHeader(status int) {
if !w.wroteHeader {
if status >= http.StatusOK && status < http.StatusMultipleChoices {
w.Header().Del("Set-Cookie")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes every Set-Cookie header on the response. It looks like deleting "all cookies" and deleting "_csrf" do the same thing today, but might be worth narrowing the delete to future proof this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, do we need to mint the cookie on these routes at all 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSRF middleware runs globally, so yeah these were getting attached to immutable assets as well in my testing.

If we narrow it via this deletion line, I think we need to do more than just restrict the deletion to the _csrf cookie. The first thing that comes to mind in the future would be session refresh or auth cookie. If we keep these cookies and make the asset immutable, then that's the same problem, arguably worse.

I think the proper future narrowing solution may be to prevent auth/session middleware from running on immutable static routes at all (if possible).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, just re-read your second comment: no, we don't need mint the cookie on these routes at all.

Comment thread server/server/route/ui.go

func (w *immutableAssetResponseWriter) WriteHeader(status int) {
if !w.wroteHeader {
if status >= http.StatusOK && status < http.StatusMultipleChoices {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we account for a 304 here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to track down whether or not a 304 is actually even reachable in this configuration.

My understanding is that go strips metadata including timestamps for embedded assets.

Either way, this is a safe follow up I think. Will make another PR to address this.

@ardiewen ardiewen Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#4056

@laurakwhit Great catch! there actually was an edge case for this: when uiAssetsPath is set, we're no longer using Go's embedded assets, and filesystem assets do respond with timestamps.

@ardiewen ardiewen mentioned this pull request Oct 20, 2026
7 of 9 tasks
rossnelson added a commit that referenced this pull request Oct 21, 2026
Auto-generated version bump from 2.56.0 to 2.57.0

Bump type: minor
go.temporal.io/api: upgraded since last version (minimum minor)

Changes included:
- [`e15c805a`](e15c805) fix(nexus): show error alert when starting a standalone Nexus operation fails (FE-1083) (#4026)
- [`dad76961`](dad7696) feat(io): add button and button-link components (FE-1090) (#4028)
- [`8ecbc7e5`](8ecbc7e) Add tooltip to truncated filter chips (#4034)
- [`2a687175`](2a68717) Allow text selection in workflow descriptions in relationships (#4035)
- [`4c440b26`](4c440b2) refactor(io): centralize chip and badge styling (FE-1095) (#4029)
- [`beccfa52`](beccfa5) Reuse badge styling and grouped corners for tags (FE-1093) (#4030)
- [`e6cece8e`](e6cece8) Add reusable native select with custom arrow styling (#4036)
- [`40108cfc`](40108cf) Dependency Inject Authz decisions through the provider (#4031)
- [`ea31abca`](ea31abc) Add persistent environment banner with preset colors (#4039)
- [`bd5c6600`](bd5c660) Fix timeline summary typography (#4043)
- [`0ad513c2`](0ad513c) fix(schedules): preserve existing headers when editing (#4051)
- [`7eb3a834`](7eb3a83) Hide focus outline on main content landmark (#4052)
- [`0ee98978`](0ee9897) Add client-side sorting to API-paginated tables (#4037)
- [`c80a75e8`](c80a75e) Wrap long link values in expanded event details (#4053)
- [`51606c50`](51606c5) Remove trailing space inside Link (#4054)
- [`23e6c4d1`](23e6c4d) Improve sorting performance for large workflow tables (#4055)
- [`45015193`](4501519) [FE-1026] Cache immutable self-hosted UI assets (#4011)
- [`25a98476`](25a9847) Upgrade build tooling to Vite 8 (#4038)
- [`b911d0a6`](b911d0a) [FE-1026] Cache immutable 304 assets (#4056)
- [`fa6e6ccc`](fa6e6cc) Bump go.temporal.io/api to v1.64.0 in the server (#4059)
- [`a654962e`](a654962) Bump Node to 22.23.4 and Go to 1.26.10 for security fixes (#4058)

Co-authored-by: rossnelson <146089+rossnelson@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 28da34f5 Deployed Oct 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants