Bug report

The following Code Snippet worked perfectly within Spring Boot 2.7.6

@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
  private static final String[] ANGULAR_RESOURCES = {
      "/favicon.ico",
      "/main.*.js",
      "/polyfills.*.js",
      "/runtime.*.js",
      "/styles.*.css",
      "/deeppurple-amber.css",
      "/indigo-pink.css",
      "/pink-bluegrey.css",
      "/purple-green.css",
      "/3rdpartylicenses.txt"
  };
  private static final List<Locale> SUPPORTED_LANGUAGES = List.of(Locale.GERMAN, Locale.ENGLISH);
  private static final Locale DEFAULT_LOCALE = Locale.ENGLISH;

  private final String prefix;
  private final Collection<HttpMessageConverter<?>> messageConverters;

  public WebMvcConfiguration(@Value("${spring.thymeleaf.prefix:" + ThymeleafProperties.DEFAULT_PREFIX + "}") String prefix, Collection<HttpMessageConverter<?>> messageConverters) {
    this.prefix = StringUtils.appendIfMissing(prefix, "/");
    this.messageConverters = messageConverters;
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.setOrder(1);
    registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/");
    for (Locale language : SUPPORTED_LANGUAGES) {
      final var relativeAngularResources = Stream.of(ANGULAR_RESOURCES)
                                                 .filter(resource -> StringUtils.contains(resource, "*"))
                                                 .map(resource -> "/" + language.getLanguage() + resource)
                                                 .toArray(String[]::new);
      registry.addResourceHandler(relativeAngularResources)
              .addResourceLocations(prefix + language.getLanguage() + "/");

      final var fixedAngularResources = Stream.of(ANGULAR_RESOURCES)
                                              .filter(resource -> !StringUtils.contains(resource, "*"))
                                              .map(resource -> "/" + language.getLanguage() + resource)
                                              .toArray(String[]::new);
      registry.addResourceHandler(fixedAngularResources)
              .addResourceLocations(prefix);

      registry.addResourceHandler("/" + language.getLanguage() + "/assets/**")
              .addResourceLocations(prefix + language.getLanguage() + "/assets/");
    }
  }

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.setOrder(2);
    for (Locale language : SUPPORTED_LANGUAGES) {
      registry.addViewController("/" + language.getLanguage() + "/**").setViewName(language.getLanguage() + "/index");
    }
  }

  @Bean
  public RouterFunction<ServerResponse> routerFunction() {
    return route(GET("/"), this::defaultLandingPage);
  }

  private ServerResponse defaultLandingPage(ServerRequest request) {
    final var locale = Optional.ofNullable(Locale.lookup(request.headers().acceptLanguage(), SUPPORTED_LANGUAGES))
                               .orElse(DEFAULT_LOCALE);
    return ServerResponse.status(HttpStatus.TEMPORARY_REDIRECT).render("redirect:/" + locale.getLanguage());
  }

  @Override
  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.defaultContentType(APPLICATION_JSON);
  }

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.addAll(messageConverters);
  }
}

Unfortunatly for Spring Boot 3.0.0 all Wildcard Resources (i.e. main.b35ffcc13cda91f0.js) wouldn't be served. Instead it will result in a HTTP 404 (Not Found).

Comment From: bclozel

Your code snippet is missing critical information that we need to better understand this case. This doesn't show the resource handling URL pattern nor the request performed.

Could you share a sample application that reproduces the issue? Ideally, an application generated from start.spring.io, containing a single resource handling registration and a single resource.

Thanks!

Comment From: mufasa1976

This is already a Github hosted Project: Calcmaster

It is as simple as possible because with this Sample I will demonstrate the possibility to serve an Angular Frontend Application by a Spring Boot Application.

By calling the URL /en/index.html you will see that all Javascript Libraries will result in a HTTP 404 (unfortunatly the used hash will depend on the Machine where the Code will be built why I use the pattern main.*.js)

Comment From: bclozel

Sorry but there's too much going on there: multi-modules project, docker compose, angular application... Could you reduce it to a simpler repro as requested?

Comment From: mufasa1976

I just made a simpler Sample which is still a multi-module Maven Project but with a precompiled Angular Application. And I removed everything else (Docker Compose, other Maven Plugins, ...)

Comment From: bclozel

Thanks for the sample, I've reduced this to the following:

    @Test
    void pathWithinMappingWithPathPattern() {
        String pattern = "/docs/cvs/file.*.html";
        String match = "/docs/cvs/file.sha.html";
        String expectedPath = "file.sha.html";

        PathPatternParser parser = new PathPatternParser();
        PathPattern parsedPattern = parser.parse(pattern);
        String s = parsedPattern.extractPathWithinPattern(toPathContainer(match)).value();
        assertThat(s).isEqualTo(expectedPath);
    }

This is a bug in Spring Framework, I'm transferring the issue as a result. This has been there for a long time (including in Spring Boot 7), but since your application disables Spring Boot web configuration with @EnableWebMvc, you were relying so far on Framework defaults. Those defaults have changed in Spring Framework 6 with spring-projects/spring-framework#28607.

To be more precise the issue only happens when getting the path within the handler mapping and the mapping itself contains a .* in the middle of it. In this example, /en/runtime.*.js triggers the issue but /en/runtime*.js will not.