From a13ecbd3cc2caff7c4c5b9d5f2a74c6d95b51807 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 24 Feb 2026 10:37:08 +0100 Subject: [PATCH] fix: prevent browser from caching API responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without explicit Cache-Control headers, browsers may heuristically cache API JSON responses. This causes stale data to be served on normal page refresh (F5) — for example, projects newly shared with a team not appearing until the user performs a hard refresh (Ctrl+Shift+R). Add Cache-Control: no-store to all API responses via middleware and configure the service worker's NetworkOnly strategy to explicitly bypass the browser HTTP cache for API requests. Ref: https://community.vikunja.io/t/team-members-cannot-see-project/1876 --- frontend/src/sw.ts | 8 ++++++-- pkg/routes/routes.go | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/frontend/src/sw.ts b/frontend/src/sw.ts index edad75436..07c26685f 100644 --- a/frontend/src/sw.ts +++ b/frontend/src/sw.ts @@ -20,10 +20,14 @@ workbox.routing.registerRoute( new workbox.strategies.StaleWhileRevalidate(), ) -// Always send api requests through the network +// Always send api requests through the network and bypass the browser's HTTP cache workbox.routing.registerRoute( new RegExp('api\\/v1\\/.*$'), - new workbox.strategies.NetworkOnly(), + new workbox.strategies.NetworkOnly({ + fetchOptions: { + cache: 'no-store', + }, + }), ) // This code listens for the user's confirmation to update the app. diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index d078352e0..4b35ee9a3 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -283,6 +283,17 @@ func collectRoutesForAPITokens(e *echo.Echo) { func registerAPIRoutes(a *echo.Group) { + // Prevent browsers from caching API responses. Without an explicit + // Cache-Control header browsers may heuristically cache JSON responses + // which causes stale data (e.g. newly team-shared projects not appearing + // until a hard refresh). + a.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Response().Header().Set("Cache-Control", "no-store") + return next(c) + } + }) + // This is the group with no auth // It is its own group to be able to rate limit this based on different heuristics n := a.Group("")