20

I want to configure my Spring Boot app to redirect any 404 not found request to my single page app.

For example if I am calling localhost:8080/asdasd/asdasdasd/asdasd which is does not exist, it should redirect to localhost:8080/notFound.

The problem is that I have a single page react app and it runs in the root path localhost:8080/. So spring should redirect to localhost:8080/notFound and then forward to / (to keep route).

SiHa
  • 7,830
  • 13
  • 34
  • 43
Vololodymyr
  • 1,996
  • 5
  • 26
  • 45
  • Is your Spring Boot app serving stating resources; if so, did you do something special in Boot to configure that or are you relying on the defaults? Don't you think this behavior would be incorrect, replying to *all* requests with HTTP 200 OK and your index page, even if the resource clearly doesn't exist? – Brian Clozel Jun 22 '17 at 08:05
  • I guess you should consider using React to redirect to unknown page, see [this questions](https://stackoverflow.com/q/32128978/1126831) for details – ledniov Jun 22 '17 at 09:13
  • 3
    Does this answer your question? [Spring boot with redirecting with single page angular2](https://stackoverflow.com/questions/43913753/spring-boot-with-redirecting-with-single-page-angular2) – Vikash Gupta Apr 14 '20 at 20:34

6 Answers6

39

This is the full Spring Boot 2.0 example:

@Configuration
public class WebApplicationConfig implements WebMvcConfigurer {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/notFound").setViewName("forward:/index.html");
}


@Bean
public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> containerCustomizer() {
    return container -> {
        container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,
                "/notFound"));
    };
  }

}
  • Okey, seems to work. However, that returns the index.html even if a backend request returns a 404. How can I avoid that? – dave0688 Sep 06 '18 at 11:03
  • 1
    @dave0688 - you could use a standard rest controller request mapping that matches against a set of well known paths (ie. angular routes etc.) in your application and return forward index.html using that: `@Controller public class Angular2Html5PathController { @RequestMapping( method = {RequestMethod.OPTIONS, RequestMethod.GET}, path = {"/path1/**", "/path2/**", "/"} ) public String forwardAngularPaths() { return "forward:/index.html"; } } ` – joensson Nov 07 '18 at 18:50
  • @joensson - this should be accepted answer as it keeps possibility for 404 page. Thanks. – Maksim Maksimov Nov 19 '18 at 10:34
  • @MaksimMaksimov Thanks, I added it as a suggested answer to this page. (Though technically the OP specifically asks for a solution that redirects any 404, so he might not agree :-)) – joensson Nov 19 '18 at 14:18
  • `forward:/index.html` causes index.html page to be displayed with spring boot not 404 page/component in SPA. – Amith Kumar Mar 27 '20 at 00:35
  • This solution works but will still send 404 if you directly access a single page sub-route. In order to fix that you can set the status code on the ViewControllerRegistration: `ViewControllerRegistration registration = registry.addViewController("/notFound"); registration.setViewName("forward:/index.html"); registration.setStatusCode(HttpStatus.OK);` – fischermatte Oct 06 '22 at 14:30
30

This should do the trick: Add an error page for 404 that routes to /notFound, and forward that to your SPA (assuming the entry is on /index.html):

@Configuration
public class WebApplicationConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/notFound").setViewName("forward:/index.html");
    }


    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {
        return container -> {
            container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,
                    "/notFound"));
        };
    }

}
Ralf Stuckert
  • 2,120
  • 14
  • 17
14

In case anyone stumbles here looking for how to handle Angular/React/other routes and paths in a Spring Boot app - but not always return index.html for any 404 - it can be done in a standard Spring controller RequestMapping. This can be done without adding view controllers and/or customizing the container error page.

The RequestMapping supports wild cards, so you can make it match against a set of well known paths (ie. angular routes etc.) in your application and only then return forward index.html:

@Controller 
public class Html5PathsController { 

    @RequestMapping( method = {RequestMethod.OPTIONS, RequestMethod.GET}, path = {"/path1/**", "/path2/**", "/"} )
    public String forwardAngularPaths() { 
        return "forward:/index.html"; 
    } 
}

Another option (borrowed from an old Spring article here: https://spring.io/blog/2015/05/13/modularizing-the-client-angular-js-and-spring-security-part-vii) is to use a naming convention:

@Controller 
public class Html5PathsController { 

    @RequestMapping(value = "/{[path:[^\\.]*}")
    public String redirect() {
        return "forward:/index.html";
    } 
}

The above configuration will match all paths that do not contain a period and are not already mapped to another controller.

joensson
  • 1,967
  • 1
  • 22
  • 18
  • Does this only match paths on the root path such as `/`, `/about` etc but not `/cars/berlin` ? – Stefan Falk Dec 21 '18 at 08:57
  • If I change this to `"/{[path:[^\.]*}/**"` I can navigate to child routes but CSS files are not loaded anymore. :/ – Stefan Falk Dec 21 '18 at 09:06
  • `@RequestMapping(value = "/{[path:[^\\.]*}")` or similar is not working for me. Did someone get something functional? – GarouDan Jan 25 '19 at 12:30
  • 1
    @GarouDan I think there may be a syntax error in the spring blog post. Try removing the [ before path - @RequestMapping(value = "/{path:[^\\.]*}"). If that doesn't help I suggest you have a look at this thread https://github.com/spring-guides/tut-spring-security-and-angular-js/issues/68#issuecomment-187675742 - they discuss this very issue. The comment I link to shows how to use a servlet filter instead e.g. – joensson Jan 29 '19 at 00:25
  • 2
    To fix solution for nested paths you can use something like this `@RequestMapping({ "/{path:[^\\.]*}", "/{path1:[^\\.]*}/{path2:[^\\.]*}", ..., "/{path1:[^\\.]*}/{path2:[^\\.]*}/{path3:[^\\.]*}/{path4:[^\\.]*}/{path5:[^\\.]*}"})` – Ivan M. Feb 10 '19 at 12:41
  • Another option worked for me. Thanks – gschambial Apr 02 '19 at 11:51
  • This should be the accepted answer, it's short simple & sweet. Oh and it works :) – chrisinmtown Aug 13 '19 at 14:39
3
//add this controller : perfect solution(from jhipster)
@Controller
public class ClientForwardController {
    @GetMapping(value = "/**/{path:[^\\.]*}")
    public String forward() {
        return "forward:/";
    }
}
Mechria Rafik
  • 87
  • 1
  • 3
  • 3
    While this code snippet may solve the problem, it doesn't explain why or how it answers the question. Please [include an explanation for your code](//meta.stackexchange.com/q/114762/269535), as that really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – Samuel Philipp Sep 04 '19 at 20:35
  • We shoud always try to refrain from using regex at such places as it is vulnerable to ReDOS attacks. – Arvind Kumar Jun 16 '20 at 08:37
  • Some advantages of this solution compared to other answers: * Simple solution (no @Bean overriding, extending or implementing needed) * Does not use deprecated classes (e.g. `WebConfigurerAdapter`) * Handles subpaths (e.g. /users/42) * Does not require keeping a list of client routes in sync with backend (e.g. `path = {/path1/**, /path2/**}` * Keeps the standard 404 handling for backend requests – stian Nov 19 '21 at 10:01
  • 2
    This throws an error. You can't add patters after wildcards? –  Apr 22 '22 at 07:37
  • 1
    throws an error with spring boot 2.6 at least: `Invalid mapping pattern detected: /**/{path:[^\\.]+} No more pattern data allowed after {*...} or ** pattern element` – fischermatte Oct 06 '22 at 13:17
1

Here the security configuration (SecurityConfig.java)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private Environment env;

    @Autowired
    private UserSecurityService userSecurityService;

    private BCryptPasswordEncoder passwordEncoder() {
        return SecurityUtility.passwordEncoder();
    }

    private static final String[] PUBLIC_MATCHERS = {
            "/css/**",
            "/js/**",
            "/data/**",
            "/sound/**",
            "/img/**",
            "/",
            "/login",
            "/logout,
            "/error",
            "/index2",
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests().
        /*  antMatchers("/**").*/
            antMatchers(PUBLIC_MATCHERS).
            permitAll().anyRequest().authenticated();
        //.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/login");

        http
            .csrf().disable().cors().disable()
            .formLogin().failureUrl("/login?error")
            .defaultSuccessUrl("/index2")
            .loginPage("/login").permitAll()
            .and()
            .logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/?logout").deleteCookies("remember-me").permitAll()
            .and()
            .rememberMe()
            .and()
            .sessionManagement().maximumSessions(3600)
            .and().
            invalidSessionUrl("/login");
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
    }
}

If not found any resource redirect to error page

@Controller
public class IndexController implements ErrorController{

    private static final String PATH = "/error";

    @RequestMapping(value = PATH)
    public String error() {
        return PATH;
    }

    @Override
    public String getErrorPath() {
        return PATH;
    }
}

Error page like

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1000/xhtml"
    xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <meta http-equiv="refresh" content="5;url=/login" />
<body>
 <h1>Page not found please login the system!</h1>
</body>
</html>
Md. Maidul Islam
  • 564
  • 6
  • 10
1

Simply implementing the org.springframework.boot.web.servlet.error.ErrorController did the trick for me. I use SpringBoot 2.0 with React. (If you are interested in how to do that here is a boilerplate project made by me: https://github.com/archangel1991/react-with-spring)

@Controller
public class CustomErrorController implements ErrorController {

    @Override
    public String getErrorPath() {
        return "/error";
    }
}

I am not sure why is this working though.

Márk Farkas
  • 1,426
  • 1
  • 12
  • 25
  • If you are using Spring Boot 2 with auto configuration then you should actually be able to delete that CustomErrorController entirely. If you look in ErrorMvcAutoConfiguration you can see that it has a conditional bean that creates a BasicErrorController if no ErrorController implementation is found. And in BasicErrorController it has this request mapping ```@RequestMapping("${server.error.path:${error.path:/error}}")``` - so if you do not specify server.error.path or error.path properties in your configuration, then it should actually default to /error. – joensson Aug 15 '19 at 08:01