From 3c6051feff0485c1a36c47ab1ddffe1562e68a3c Mon Sep 17 00:00:00 2001 From: ulrichschulte Date: Thu, 10 Sep 2026 16:41:26 +0200 Subject: [PATCH 1/3] #5662: fix content-type sba-settings --- .../boot/admin/server/ui/web/UiController.java | 2 +- .../server/ui/AdminUiServletApplicationTest.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java index 8962f2fa00b..a139e8bb879 100644 --- a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java +++ b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java @@ -115,7 +115,7 @@ public String index() { return "index"; } - @GetMapping(path = "/sba-settings.js", produces = "application/javascript") + @GetMapping(path = "/sba-settings.js") public String sbaSettings() { return "sba-settings.js"; } diff --git a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java index 9ebcc2dbfe6..a72290b0749 100644 --- a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java +++ b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AdminUiServletApplicationTest.java @@ -29,6 +29,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import de.codecentric.boot.admin.server.config.EnableAdminServer; @@ -73,6 +75,16 @@ protected SecurityFilterChain filterChain(HttpSecurity http) { .build(); } + @Bean + WebMvcConfigurer defaultJsonContentNegotiation() { + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.defaultContentType(MediaType.APPLICATION_JSON); + } + }; + } + } } From 14d3d71b34f2a8fc71c8d9049011f9626305a6a9 Mon Sep 17 00:00:00 2001 From: ulrichschulte Date: Fri, 18 Sep 2026 23:01:26 +0200 Subject: [PATCH 2/3] Inject ObjectProvider to avoid breaking lightweight auto-config contexts --- .../AdminServerUiAutoConfiguration.java | 7 +++++-- .../admin/server/ui/web/UiController.java | 21 +++++++++++++++---- .../admin/server/ui/web/UiControllerTest.java | 17 ++++++++++++--- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java index 1e4468c6a27..6fe21270701 100644 --- a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java +++ b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -38,6 +39,7 @@ import org.springframework.http.MediaType; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.thymeleaf.ITemplateEngine; import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.templatemode.TemplateMode; @@ -92,7 +94,8 @@ public AdminServerUiAutoConfiguration(AdminServerUiProperties adminUi, AdminServ @Bean @ConditionalOnMissingBean - public UiController homeUiController(UiExtensions uiExtensions) throws IOException { + public UiController homeUiController(UiExtensions uiExtensions, ObjectProvider templateEngine) + throws IOException { List extensionRoutes = new UiRoutesScanner(this.applicationContext) .scan(this.adminUi.getExtensionResourceLocations()); List routes = Stream.concat(DEFAULT_UI_ROUTES.stream(), extensionRoutes.stream()).toList(); @@ -120,7 +123,7 @@ public UiController homeUiController(UiExtensions uiExtensions) throws IOExcepti String publicUrl = (this.adminUi.getPublicUrl() != null) ? this.adminUi.getPublicUrl() : this.adminServer.getContextPath(); - return new UiController(publicUrl, uiExtensions, uiSettings); + return new UiController(publicUrl, uiExtensions, uiSettings, templateEngine); } @Bean diff --git a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java index a57619ac26f..80dec1d60b8 100644 --- a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java +++ b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java @@ -18,6 +18,7 @@ import java.security.Principal; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -25,12 +26,17 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import org.thymeleaf.ITemplateEngine; +import org.thymeleaf.context.Context; import de.codecentric.boot.admin.server.ui.config.AdminServerUiProperties.Palette; import de.codecentric.boot.admin.server.ui.config.AdminServerUiProperties.PollTimer; @@ -53,10 +59,14 @@ public class UiController { private final Settings uiSettings; - public UiController(String publicUrl, UiExtensions uiExtensions, Settings uiSettings) { + private final ObjectProvider templateEngine; + + public UiController(String publicUrl, UiExtensions uiExtensions, Settings uiSettings, + ObjectProvider templateEngine) { this.publicUrl = publicUrl; this.uiExtensions = uiExtensions; this.uiSettings = uiSettings; + this.templateEngine = templateEngine; } @ModelAttribute(value = "baseUrl", binding = false) @@ -115,9 +125,12 @@ public String index() { return "index"; } - @GetMapping(path = "/sba-settings.js") - public String sbaSettings() { - return "sba-settings.js"; + @GetMapping(path = "/sba-settings.js", produces = { "application/javascript", MediaType.APPLICATION_JSON_VALUE }) + public ResponseEntity sbaSettings(Model model, Locale locale) { + Context context = new Context(locale, model.asMap()); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType("application/javascript")) + .body(this.templateEngine.getObject().process("sba-settings.js", context)); } @GetMapping(path = "/variables.css", produces = "text/css") diff --git a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java index 3c29b74134a..e6b651e754d 100644 --- a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java +++ b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java @@ -25,9 +25,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.AbstractView; +import org.thymeleaf.ITemplateEngine; +import org.thymeleaf.TemplateEngine; import de.codecentric.boot.admin.server.ui.config.AdminServerUiProperties; import de.codecentric.boot.admin.server.ui.config.CssColorUtils; @@ -125,7 +129,7 @@ void should_populate_palette_with_matching_rgb_colors_for_all_grades() { theme.setPalette(palette); UiController.Settings settings = UiController.Settings.builder().theme(theme).build(); - UiController controller = new UiController("", UiExtensions.EMPTY, settings); + UiController controller = new UiController("", UiExtensions.EMPTY, settings, templateEngine()); Map actualPalette = controller.getPalette(); @@ -205,19 +209,26 @@ private MockMvc setupController(String publicUrl, List new AdminControllerHandlerMapping("")) .build(); } private MockMvc setupControllerWithView(String publicUrl, UiExtensions uiExtensions, UiController.Settings uiSettings) { - return MockMvcBuilders.standaloneSetup(new UiController(publicUrl, uiExtensions, uiSettings)) + return MockMvcBuilders.standaloneSetup(new UiController(publicUrl, uiExtensions, uiSettings, templateEngine())) .setCustomHandlerMapping(() -> new AdminControllerHandlerMapping("")) .setSingleView(new NoOpView()) .build(); } + private static ObjectProvider templateEngine() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("templateEngine", new TemplateEngine()); + return beanFactory.getBeanProvider(ITemplateEngine.class); + } + private static final class NoOpView extends AbstractView { @Override From c89e0dd85b01a147774dba4b36a4dc690e6b86ec Mon Sep 17 00:00:00 2001 From: ulrichschulte Date: Fri, 18 Sep 2026 23:40:38 +0200 Subject: [PATCH 3/3] Fix #5662 without changing WebFlux behavior. - Reverted UiController.sbaSettings() to normal Thymeleaf view rendering. - Preserves Spring/Thymeleaf web context. - Avoids _csrf regression. - Removed misleading JSON produces. - /sba-settings.js again truthfully produces only application/javascript. - Explicit Accept: application/json now returns 406. - Added servlet-only filter for /sba-settings.js. - Rewrites missing/blank/wildcard Accept to application/javascript. - Fixes #5662 without changing WebFlux behavior. - Added regression test for explicit JSON accept. --- .../AdminServerUiAutoConfiguration.java | 91 ++++++++++++++++++- .../admin/server/ui/web/UiController.java | 21 +---- .../ui/AbstractAdminUiApplicationTest.java | 11 +++ .../admin/server/ui/web/UiControllerTest.java | 17 +--- 4 files changed, 104 insertions(+), 36 deletions(-) diff --git a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java index 6fe21270701..a9e73a97646 100644 --- a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java +++ b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/config/AdminServerUiAutoConfiguration.java @@ -18,28 +18,38 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.webflux.autoconfigure.WebFluxProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.thymeleaf.ITemplateEngine; import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.templatemode.TemplateMode; @@ -94,8 +104,7 @@ public AdminServerUiAutoConfiguration(AdminServerUiProperties adminUi, AdminServ @Bean @ConditionalOnMissingBean - public UiController homeUiController(UiExtensions uiExtensions, ObjectProvider templateEngine) - throws IOException { + public UiController homeUiController(UiExtensions uiExtensions) throws IOException { List extensionRoutes = new UiRoutesScanner(this.applicationContext) .scan(this.adminUi.getExtensionResourceLocations()); List routes = Stream.concat(DEFAULT_UI_ROUTES.stream(), extensionRoutes.stream()).toList(); @@ -123,7 +132,7 @@ public UiController homeUiController(UiExtensions uiExtensions, ObjectProvider sbaSettingsAcceptHeaderFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>( + new SbaSettingsAcceptHeaderFilter()); + registration.addUrlPatterns(this.adminServer.path("/sba-settings.js")); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } + + private static final class SbaSettingsAcceptHeaderFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + filterChain.doFilter( + shouldRewriteAcceptHeader(request) ? new AcceptHeaderRequestWrapper(request) : request, + response); + } + + private static boolean shouldRewriteAcceptHeader(HttpServletRequest request) { + String accept = request.getHeader(HttpHeaders.ACCEPT); + if (accept == null || accept.isBlank()) { + return true; + } + try { + return MediaType.parseMediaTypes(accept) + .stream() + .allMatch((mediaType) -> "*".equals(mediaType.getType()) + && "*".equals(mediaType.getSubtype())); + } + catch (IllegalArgumentException ex) { + return false; + } + } + + } + + private static final class AcceptHeaderRequestWrapper extends HttpServletRequestWrapper { + + private static final String ACCEPT = "application/javascript"; + + private AcceptHeaderRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public String getHeader(String name) { + if (HttpHeaders.ACCEPT.equalsIgnoreCase(name)) { + return ACCEPT; + } + return super.getHeader(name); + } + + @Override + public Enumeration getHeaders(String name) { + if (HttpHeaders.ACCEPT.equalsIgnoreCase(name)) { + return Collections.enumeration(List.of(ACCEPT)); + } + return super.getHeaders(name); + } + + @Override + public Enumeration getHeaderNames() { + List names = Collections.list(super.getHeaderNames()); + if (names.stream().noneMatch(HttpHeaders.ACCEPT::equalsIgnoreCase)) { + names.add(HttpHeaders.ACCEPT); + } + return Collections.enumeration(names); + } + + } + } } diff --git a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java index 80dec1d60b8..0fc2c92ca04 100644 --- a/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java +++ b/spring-boot-admin-server-ui/src/main/java/de/codecentric/boot/admin/server/ui/web/UiController.java @@ -18,7 +18,6 @@ import java.security.Principal; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -26,17 +25,12 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import org.thymeleaf.ITemplateEngine; -import org.thymeleaf.context.Context; import de.codecentric.boot.admin.server.ui.config.AdminServerUiProperties.Palette; import de.codecentric.boot.admin.server.ui.config.AdminServerUiProperties.PollTimer; @@ -59,14 +53,10 @@ public class UiController { private final Settings uiSettings; - private final ObjectProvider templateEngine; - - public UiController(String publicUrl, UiExtensions uiExtensions, Settings uiSettings, - ObjectProvider templateEngine) { + public UiController(String publicUrl, UiExtensions uiExtensions, Settings uiSettings) { this.publicUrl = publicUrl; this.uiExtensions = uiExtensions; this.uiSettings = uiSettings; - this.templateEngine = templateEngine; } @ModelAttribute(value = "baseUrl", binding = false) @@ -125,12 +115,9 @@ public String index() { return "index"; } - @GetMapping(path = "/sba-settings.js", produces = { "application/javascript", MediaType.APPLICATION_JSON_VALUE }) - public ResponseEntity sbaSettings(Model model, Locale locale) { - Context context = new Context(locale, model.asMap()); - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType("application/javascript")) - .body(this.templateEngine.getObject().process("sba-settings.js", context)); + @GetMapping(path = "/sba-settings.js", produces = "application/javascript") + public String sbaSettings() { + return "sba-settings.js"; } @GetMapping(path = "/variables.css", produces = "text/css") diff --git a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java index f5c15632e10..ce1648b2651 100644 --- a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java +++ b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/AbstractAdminUiApplicationTest.java @@ -135,6 +135,17 @@ public void should_contain_only_one_language() { //@formatter:on } + @Test + public void should_not_return_sba_settings_for_json_accept() { + //@formatter:off + this.webClient.get() + .uri("/sba-settings.js") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isEqualTo(HttpStatus.NOT_ACCEPTABLE); + //@formatter:on + } + @Test public void should_return_defaults_for_pollTimers() { //@formatter:off diff --git a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java index e6b651e754d..3c29b74134a 100644 --- a/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java +++ b/spring-boot-admin-server-ui/src/test/java/de/codecentric/boot/admin/server/ui/web/UiControllerTest.java @@ -25,13 +25,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.AbstractView; -import org.thymeleaf.ITemplateEngine; -import org.thymeleaf.TemplateEngine; import de.codecentric.boot.admin.server.ui.config.AdminServerUiProperties; import de.codecentric.boot.admin.server.ui.config.CssColorUtils; @@ -129,7 +125,7 @@ void should_populate_palette_with_matching_rgb_colors_for_all_grades() { theme.setPalette(palette); UiController.Settings settings = UiController.Settings.builder().theme(theme).build(); - UiController controller = new UiController("", UiExtensions.EMPTY, settings, templateEngine()); + UiController controller = new UiController("", UiExtensions.EMPTY, settings); Map actualPalette = controller.getPalette(); @@ -209,26 +205,19 @@ private MockMvc setupController(String publicUrl, List new AdminControllerHandlerMapping("")) .build(); } private MockMvc setupControllerWithView(String publicUrl, UiExtensions uiExtensions, UiController.Settings uiSettings) { - return MockMvcBuilders.standaloneSetup(new UiController(publicUrl, uiExtensions, uiSettings, templateEngine())) + return MockMvcBuilders.standaloneSetup(new UiController(publicUrl, uiExtensions, uiSettings)) .setCustomHandlerMapping(() -> new AdminControllerHandlerMapping("")) .setSingleView(new NoOpView()) .build(); } - private static ObjectProvider templateEngine() { - StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); - beanFactory.addBean("templateEngine", new TemplateEngine()); - return beanFactory.getBeanProvider(ITemplateEngine.class); - } - private static final class NoOpView extends AbstractView { @Override