mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
perf(api): optimize static asset caching, enable ETag support, and expand response compression mime types. (#6273)
This commit is contained in:
@@ -21,6 +21,13 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
|||||||
HttpServletRequest request, HttpServletResponse response, Object handler)
|
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
String requestURI = request.getRequestURI();
|
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);
|
boolean isEnabled = endpointConfiguration.isEndpointEnabledForUri(requestURI);
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
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 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
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(endpointInterceptor);
|
registry.addInterceptor(endpointInterceptor);
|
||||||
@@ -31,37 +38,95 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
// Cache hashed assets (JS/CSS with content hashes) for 1 year
|
String staticPath =
|
||||||
// These files have names like index-ChAS4tCC.js that change when content changes
|
"file:"
|
||||||
// Check customFiles/static first, then fall back to classpath
|
+ 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/**")
|
registry.addResourceHandler("/assets/**")
|
||||||
.addResourceLocations(
|
.addResourceLocations(staticPath + "assets/", "classpath:/static/assets/")
|
||||||
"file:"
|
.setCacheControl(IMMUTABLE_ONE_YEAR)
|
||||||
+ stirling.software.common.configuration.InstallationPathConfig
|
.resourceChain(true);
|
||||||
.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
|
// 3. Media and fonts (immutable)
|
||||||
// Note: index.html is handled by ReactRoutingController for dynamic processing
|
registry.addResourceHandler("/images/**", "/fonts/**")
|
||||||
registry.addResourceHandler("/index.html")
|
|
||||||
.addResourceLocations(
|
.addResourceLocations(
|
||||||
"file:"
|
staticPath + "images/",
|
||||||
+ stirling.software.common.configuration.InstallationPathConfig
|
"classpath:/static/images/",
|
||||||
.getStaticPath(),
|
staticPath + "fonts/",
|
||||||
"classpath:/static/")
|
"classpath:/static/fonts/")
|
||||||
.setCacheControl(CacheControl.noCache().mustRevalidate());
|
.setCacheControl(IMMUTABLE_ONE_YEAR)
|
||||||
|
.resourceChain(true);
|
||||||
|
|
||||||
// Handle all other static resources (js, css, images, fonts, etc.)
|
// 4. Branding and stable non-fingerprinted assets (1 day + SWR)
|
||||||
// Check customFiles/static first for user overrides
|
// 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("/**")
|
registry.addResourceHandler("/**")
|
||||||
.addResourceLocations(
|
.addResourceLocations(staticPath, "classpath:/static/")
|
||||||
"file:"
|
.setCacheControl(NO_CACHE)
|
||||||
+ stirling.software.common.configuration.InstallationPathConfig
|
.resourceChain(true);
|
||||||
.getStaticPath(),
|
|
||||||
"classpath:/static/")
|
|
||||||
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -115,9 +180,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
applicationProperties.getSystem().getCorsAllowedOrigins());
|
applicationProperties.getSystem().getCorsAllowedOrigins());
|
||||||
|
|
||||||
// Combine user-configured origins with Tauri origins
|
// Combine user-configured origins with Tauri origins
|
||||||
java.util.List<String> allOrigins =
|
List<String> allOrigins =
|
||||||
new java.util.ArrayList<>(
|
new ArrayList<>(applicationProperties.getSystem().getCorsAllowedOrigins());
|
||||||
applicationProperties.getSystem().getCorsAllowedOrigins());
|
|
||||||
|
|
||||||
// Always include Tauri origins for desktop app compatibility
|
// Always include Tauri origins for desktop app compatibility
|
||||||
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
|
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
|
||||||
@@ -158,7 +222,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
} else {
|
} else {
|
||||||
// Default to allowing all origins when nothing is configured
|
// Default to allowing all origins when nothing is configured
|
||||||
logger.debug(
|
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("/**")
|
registry.addMapping("/**")
|
||||||
.allowedOriginPatterns("*")
|
.allowedOriginPatterns("*")
|
||||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||||
|
|||||||
+13
-3
@@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.CacheControl;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@@ -134,13 +135,22 @@ public class ReactRoutingController {
|
|||||||
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request) {
|
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request) {
|
||||||
try {
|
try {
|
||||||
if (indexHtmlExists && cachedIndexHtml != null) {
|
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)
|
// 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) {
|
} catch (Exception ex) {
|
||||||
log.error("Failed to serve index.html, returning fallback", 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ spring.security.filter.dispatcher-types=REQUEST,ERROR
|
|||||||
# Response compression
|
# Response compression
|
||||||
server.compression.enabled=true
|
server.compression.enabled=true
|
||||||
server.compression.min-response-size=1024
|
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.path=/error
|
||||||
spring.web.error.whitelabel.enabled=false
|
spring.web.error.whitelabel.enabled=false
|
||||||
|
|||||||
Reference in New Issue
Block a user