Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public boolean preHandle(
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");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package stirling.software.SPDF.config;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
Expand All @@ -24,44 +25,84 @@ 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);
}

@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(staticPath + "assets/", "classpath:/static/assets/")
.setCacheControl(IMMUTABLE_ONE_YEAR)
.resourceChain(true);

// 3. Media and fonts (immutable)
registry.addResourceHandler("/images/**", "/fonts/**")
.addResourceLocations(
"file:"
+ stirling.software.common.configuration.InstallationPathConfig
.getStaticPath()
+ "assets/",
"classpath:/static/assets/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic());

// 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")
staticPath + "images/",
"classpath:/static/images/",
staticPath + "fonts/",
"classpath:/static/fonts/")
.setCacheControl(IMMUTABLE_ONE_YEAR)
.resourceChain(true);

// 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/**")
.addResourceLocations(
"file:"
+ stirling.software.common.configuration.InstallationPathConfig
.getStaticPath(),
"classpath:/static/")
.setCacheControl(CacheControl.noCache().mustRevalidate());

// Handle all other static resources (js, css, images, fonts, etc.)
// Check customFiles/static first for user overrides
staticPath,
"classpath:/static/",
staticPath + "pdfjs/",
"classpath:/static/pdfjs/",
staticPath + "pdfjs-legacy/",
"classpath:/static/pdfjs-legacy/",
staticPath + "pdfium/",
"classpath:/static/pdfium/")
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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;
Expand Down Expand Up @@ -134,13 +135,22 @@ private Resource getIndexHtmlResource() {
public ResponseEntity<String> 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());
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/core/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading