diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index ce96bbd17..db1e718c5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -21,6 +21,13 @@ public class EndpointInterceptor implements HandlerInterceptor { HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); + + // Prevent API responses from being stored by browsers or intermediary caches by default + String servletPath = request.getServletPath(); + if (servletPath != null && servletPath.startsWith("/api/")) { + response.setHeader("Cache-Control", "private, no-store"); + } + boolean isEnabled = endpointConfiguration.isEndpointEnabledForUri(requestURI); if (!isEnabled) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 59790ee5d..48fecf001 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,5 +1,8 @@ package stirling.software.SPDF.config; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; @@ -24,6 +27,10 @@ public class WebMvcConfig implements WebMvcConfigurer { private static final Logger logger = LoggerFactory.getLogger(WebMvcConfig.class); + private static final CacheControl NO_CACHE = CacheControl.noCache(); + private static final CacheControl IMMUTABLE_ONE_YEAR = + CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable(); + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(endpointInterceptor); @@ -31,37 +38,95 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - // Cache hashed assets (JS/CSS with content hashes) for 1 year - // These files have names like index-ChAS4tCC.js that change when content changes - // Check customFiles/static first, then fall back to classpath + String staticPath = + "file:" + + stirling.software.common.configuration.InstallationPathConfig + .getStaticPath(); + + // 1. Service worker and PWA metadata (never store) + // Browsers revalidate SW bytes anyway; no-store is the safest for atomic updates. + registry.addResourceHandler( + "/sw.js", "/manifest.json", "/site.webmanifest", "/browserconfig.xml") + .addResourceLocations(staticPath, "classpath:/static/") + .setCacheControl(CacheControl.noStore()) + .resourceChain(true); + + // 2. Vite fingerprinted assets (immutable) + // These already have content hashes in filenames (e.g. index-ChAS4tCC.js) registry.addResourceHandler("/assets/**") - .addResourceLocations( - "file:" - + stirling.software.common.configuration.InstallationPathConfig - .getStaticPath() - + "assets/", - "classpath:/static/assets/") - .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic()); + .addResourceLocations(staticPath + "assets/", "classpath:/static/assets/") + .setCacheControl(IMMUTABLE_ONE_YEAR) + .resourceChain(true); - // Don't cache index.html - it needs to be fresh to reference latest hashed assets - // Note: index.html is handled by ReactRoutingController for dynamic processing - registry.addResourceHandler("/index.html") + // 3. Media and fonts (immutable) + registry.addResourceHandler("/images/**", "/fonts/**") .addResourceLocations( - "file:" - + stirling.software.common.configuration.InstallationPathConfig - .getStaticPath(), - "classpath:/static/") - .setCacheControl(CacheControl.noCache().mustRevalidate()); + staticPath + "images/", + "classpath:/static/images/", + staticPath + "fonts/", + "classpath:/static/fonts/") + .setCacheControl(IMMUTABLE_ONE_YEAR) + .resourceChain(true); - // Handle all other static resources (js, css, images, fonts, etc.) - // Check customFiles/static first for user overrides + // 4. Branding and stable non-fingerprinted assets (1 day + SWR) + // Use stale-while-revalidate to improve perceived performance. + registry.addResourceHandler( + "/favicon.*", + "/apple-touch-icon.png", + "/android-chrome-*.png", + "/mstile-*.png", + "/safari-pinned-tab.svg", + "/icons/**", + "/modern-logo/**", + "/classic-logo/**", + "/robots.txt", + "/3rdPartyLicenses.json", + "/pdfjs/**", + "/pdfjs-legacy/**", + "/pdfium/**", + "/locales/**", + "/css/**", + "/js/**", + "/vendor/**", + "/samples/**", + "/og_images/**", + "/Login/**", + "/manifest-classic.json") + .addResourceLocations( + staticPath, + "classpath:/static/", + staticPath + "pdfjs/", + "classpath:/static/pdfjs/", + staticPath + "pdfjs-legacy/", + "classpath:/static/pdfjs-legacy/", + staticPath + "pdfium/", + "classpath:/static/pdfium/", + staticPath + "locales/", + "classpath:/static/locales/", + staticPath + "css/", + "classpath:/static/css/", + staticPath + "js/", + "classpath:/static/js/", + staticPath + "vendor/", + "classpath:/static/vendor/", + staticPath + "samples/", + "classpath:/static/samples/", + staticPath + "og_images/", + "classpath:/static/og_images/", + staticPath + "Login/", + "classpath:/static/Login/") + .setCacheControl( + CacheControl.maxAge(Duration.ofDays(1)) + .cachePublic() + .staleWhileRevalidate(Duration.ofDays(7))) + .resourceChain(true); + + // 5. Catch-all (SPA fallback) + // Must check with server to ensure index.html is always fresh. registry.addResourceHandler("/**") - .addResourceLocations( - "file:" - + stirling.software.common.configuration.InstallationPathConfig - .getStaticPath(), - "classpath:/static/") - .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)); + .addResourceLocations(staticPath, "classpath:/static/") + .setCacheControl(NO_CACHE) + .resourceChain(true); } @Override @@ -115,9 +180,8 @@ public class WebMvcConfig implements WebMvcConfigurer { applicationProperties.getSystem().getCorsAllowedOrigins()); // Combine user-configured origins with Tauri origins - java.util.List allOrigins = - new java.util.ArrayList<>( - applicationProperties.getSystem().getCorsAllowedOrigins()); + List allOrigins = + new ArrayList<>(applicationProperties.getSystem().getCorsAllowedOrigins()); // Always include Tauri origins for desktop app compatibility // Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost @@ -158,7 +222,8 @@ public class WebMvcConfig implements WebMvcConfigurer { } else { // Default to allowing all origins when nothing is configured logger.debug( - "No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); WebMvcConfig allowing all origins."); + "No CORS allowed origins configured in settings.yml" + + " (system.corsAllowedOrigins); WebMvcConfig allowing all origins."); registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index 7a0779a4e..ce0f5edaf 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -134,13 +135,22 @@ public class ReactRoutingController { public ResponseEntity serveIndexHtml(HttpServletRequest request) { try { if (indexHtmlExists && cachedIndexHtml != null) { - return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(cachedIndexHtml); + return ResponseEntity.ok() + .cacheControl(CacheControl.noCache().mustRevalidate()) + .contentType(MediaType.TEXT_HTML) + .body(cachedIndexHtml); } // Fallback: process on each request (dev mode or cache failed) - return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(processIndexHtml()); + return ResponseEntity.ok() + .cacheControl(CacheControl.noCache().mustRevalidate()) + .contentType(MediaType.TEXT_HTML) + .body(processIndexHtml()); } catch (Exception ex) { log.error("Failed to serve index.html, returning fallback", ex); - return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(buildFallbackHtml()); + return ResponseEntity.ok() + .cacheControl(CacheControl.noCache().mustRevalidate()) + .contentType(MediaType.TEXT_HTML) + .body(buildFallbackHtml()); } } diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index 701adb2c2..b5777e1a1 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -33,7 +33,7 @@ spring.security.filter.dispatcher-types=REQUEST,ERROR # Response compression server.compression.enabled=true server.compression.min-response-size=1024 -server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript +server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript,image/svg+xml,application/x-font-ttf,font/opentype,application/vnd.ms-fontobject,font/woff,font/woff2,application/font-woff,application/font-woff2 spring.web.error.path=/error spring.web.error.whitelabel.enabled=false