7

My question is very similar to this question that has already been asked and answered but is not 100% up-to-date.

We used the solution from Chris Gaskill for quite some time and it suited us perfectly because we wanted to redirect requests that contain more than one path segment (i.e. /foo/bar)

From Spring Boot 2.4 on, Boot uses the PathPatternParser instead of the AntPathMatcher, wherein the former does not support ** at the start of a pattern anymore (see docs).

Is there some other solution to get the same behavior? What do you use to redirect all requests, that did not match anything else, to the index.html of the Angular app?

This is the code of the controller that forwards the requests.

@Controller
class SpaRoutingController {

  @GetMapping("/**/{path:[^\\.]*}", headers = "X-Requested-With!=XMLHttpRequest")
  fun forward(): String? {
      return "forward:/"
  }
}
M4R1KU
  • 710
  • 7
  • 22

4 Answers4

3

I would rather go solution where I configure a WebMvcConfigurer instead. Please see below for code.

Maven Configuration

Maven configuration directs frontend-maven-plugin to build angular and copy built content into target/static directory, so that it is available in final WAR and JAR built. Then I additionally configure a WebMvcConfigurer to find a resource in static directory of JAR / WAR, if found - send it to client, otherwise redirect to index.html.

<plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>${maven-clean-plugin.version}</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>src/main/webapp</directory>
              <includes>
                <include>*.js</include>
                <include>*.txt</include>
                <include>*.html</include>
                <include>*.css</include>
                <include>*.map</include>
                <include>*.json</include>
                <include>*.ico</include>
                <followSymlinks>false</followSymlinks>
              </includes>
            </fileset>
            <fileset>
              <directory>src/main/webapp/assets</directory>
              <includes>
                <include>**</include>
                <followSymlinks>false</followSymlinks>
              </includes>
            </fileset>
          </filesets>
        </configuration>
        <executions>
          <execution>
            <id>auto-clean</id>
            <phase>initialize</phase>
            <goals>
              <goal>clean</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <artifactId>maven-resources-plugin</artifactId>
        <executions>
          <execution>
            <id>copy-resources</id>
            <phase>process-resources</phase>
            <goals>
              <goal>copy-resources</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.directory}/classes/static</outputDirectory>
              <includeEmptyDirs>true</includeEmptyDirs>
              <resources>
                <resource>
                  <directory>${basedir}/src/main/${angular.project.name}/dist/${angular.project.name}</directory>
                  <filtering>true</filtering>
                </resource>
              </resources>
            </configuration>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>com.github.eirslett</groupId>
        <artifactId>frontend-maven-plugin</artifactId>
        <version>1.8.0</version>
        <configuration>
          <workingDirectory>src/main/${angular.project.name}</workingDirectory>
        </configuration>
        <executions>
          <execution>
            <id>install node and npm</id>
            <goals>
              <goal>install-node-and-npm</goal>
            </goals>
            <phase>generate-resources</phase>
            <configuration>
              <nodeVersion>v14.16.1</nodeVersion>
            </configuration>

          </execution>
          <execution>
            <id>npm build angular</id>
            <goals>
              <goal>npm</goal>
            </goals>
            <phase>generate-resources</phase>
            <configuration>
              <arguments>run build:prod</arguments>
            </configuration>
          </execution>
        </executions>
      </plugin>

MVC Configuration

Here you try to find if a resource exists in your static directory, if not redirect it to index.html.

You may have to change source location as per your requirement.

@Configuration
public class ApplicationWebMvcConfiguration implements WebMvcConfigurer {
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {

    registry.addResourceHandler("/**")
            .addResourceLocations("classpath:/static/")
            .resourceChain(true)
            .addResolver(new PathResourceResolver() {
              @Override
              protected Resource getResource(String resourcePath, Resource location)
                  throws IOException {

                Resource requestedResource = location.createRelative(resourcePath);
                if (requestedResource.exists() && requestedResource.isReadable()) {
                  return requestedResource;
                }

                return new ClassPathResource("/static/index.html");

              }
            });

  }

}
Gaurav
  • 3,614
  • 3
  • 30
  • 51
  • Your answer is not exactly equivalent to the solution with the earlier AntPathMatcher, but it is still the one that does replicate the previous behaviour the most. The biggest difference is that we do not get any JSON 404 errors when making requests through the Frontend with the X-Requested-With header. It's not a big deal, but still worth considering. – M4R1KU Mar 01 '22 at 16:35
  • Hey. May I know what other functionality you want here ? Perhaps there could be an alternate way to do it. – Gaurav Mar 01 '22 at 21:46
  • Thanks, worked for me, with this angular routes works as it should be – Юрій Яремчук Jan 20 '23 at 09:48
1

PathPatternParser equivalent to previous AntPathMatcher /** would be:

@GetMapping("/{*path}")

according to spring documentation:

/resources/{*path} — matches all files underneath the /resources/, as well as /resources, and captures their relative path in a variable named "path"; /resources/image.png will match with "path" → "/image.png", and /resources/css/spring.css will match with "path" → "/css/spring.css"

Edit - relating to loop:

in your case in order to avoid the loop due to the use of forward:/ if you only care about loading your angular page just replace forward with the actual content of your index.html

if for some reason you need to forward in order to flatten the uri before returning the response you can solve it in various manners you can either forward:/index.html @GetMapping("/index.html") which should have precedence over wildcard or use @PathVariable in order to differentiate behavior and avoid eternal forward loop

@Controller
class SpaRoutingController {

  @GetMapping("/{*path}")
  fun forward(@PathVariable(value="path", required=false) path: String): String? {
      if ("/".equals(path)) { // may need to handle additional cases
         return ""; // or your index.html
      }
      return "forward:/";
  }
}
ezer
  • 984
  • 6
  • 10
  • I already tried this, and it does not work as you have described, because the forwarded request matches the same controller and results in a StackOverflowError. If you know how to break the cycle, please update the answer :) – M4R1KU Mar 01 '22 at 16:12
0

While using Angular you can not talk about pages such as index.html. What you have it to a route !

So when you redirect, you should do is to a route not a page.

ModelAndView forward() {
        return new ModelAndView("redirect:/");
    }

PS : If / is your default route.

Nadhir Houari
  • 390
  • 4
  • 10
  • note that `redirect:` will return 302 response to the client with `Location` header for the browser to redirect to, while `forward:` occurs entirely on the server. – ezer Mar 02 '22 at 08:26
0

Have you tried using a Servlet Filter to handle endpoints not handled by Spring Boot? This article from Baeldung explains a solution which you could check for your use case.

Arun Avanathan
  • 982
  • 4
  • 11
  • 26