Configure spring boot to redirect 404 to a single page app [duplicate]
Asked Answered
B

6

21

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).

Bulletin answered 22/6, 2017 at 7:25 Comment(3)
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?Premises
I guess you should consider using React to redirect to unknown page, see this questions for detailsWindbag
Does this answer your question? Spring boot with redirecting with single page angular2Adlai
S
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"));
        };
    }

}
Suburbanize answered 22/6, 2017 at 11:2 Comment(8)
In Spring Boot V2 (currently in development, 2.0.0.M7), EmbeddedServletContainerCustomizer has changed to WebServerFactoryCustomizer. Signature for bean containerCustomizer will be WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>Submicroscopic
This is the answer! Thank you.Renick
can you obtain the same result with the @ExceptionHandler(ResourceNotFoundException.class) ?Duckboard
Edited in the version for Spring Boot 2. Works great here!Fetterlock
In Spring Boot 2.0.2.RELEASE container.addErrorPages is not a valid methodOthilia
If I add above config to my app then it starts giving 404 for already mapped controllers and redirects to index.html. How do I add this config without overriding my existing controllers?Trudy
Please see answer from @Sterling below using a simple controller.Nietzsche
Does not work on spring boot 2.3.1.Calabash
I
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"));
    };
  }

}
Illyria answered 5/8, 2018 at 22:7 Comment(6)
Okey, seems to work. However, that returns the index.html even if a backend request returns a 404. How can I avoid that?Irfan
@Irfan - 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"; } } Sterling
@Sterling - this should be accepted answer as it keeps possibility for 404 page. Thanks.Retroflex
@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 :-))Sterling
forward:/index.html causes index.html page to be displayed with spring boot not 404 page/component in SPA.Yardage
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);Tilghman
S
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"));
        };
    }

}
Suburbanize answered 22/6, 2017 at 11:2 Comment(8)
In Spring Boot V2 (currently in development, 2.0.0.M7), EmbeddedServletContainerCustomizer has changed to WebServerFactoryCustomizer. Signature for bean containerCustomizer will be WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>Submicroscopic
This is the answer! Thank you.Renick
can you obtain the same result with the @ExceptionHandler(ResourceNotFoundException.class) ?Duckboard
Edited in the version for Spring Boot 2. Works great here!Fetterlock
In Spring Boot 2.0.2.RELEASE container.addErrorPages is not a valid methodOthilia
If I add above config to my app then it starts giving 404 for already mapped controllers and redirects to index.html. How do I add this config without overriding my existing controllers?Trudy
Please see answer from @Sterling below using a simple controller.Nietzsche
Does not work on spring boot 2.3.1.Calabash
S
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.

Sterling answered 19/11, 2018 at 14:18 Comment(7)
Does this only match paths on the root path such as /, /about etc but not /cars/berlin ?Dacha
If I change this to "/{[path:[^\.]*}/**" I can navigate to child routes but CSS files are not loaded anymore. :/Dacha
@RequestMapping(value = "/{[path:[^\\.]*}") or similar is not working for me. Did someone get something functional?Electrodeposit
@Electrodeposit 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 github.com/spring-guides/tut-spring-security-and-angular-js/… - they discuss this very issue. The comment I link to shows how to use a servlet filter instead e.g.Sterling
To fix solution for nested paths you can use something like this @RequestMapping({ "/{path:[^\\.]*}", "/{path1:[^\\.]*}/{path2:[^\\.]*}", ..., "/{path1:[^\\.]*}/{path2:[^\\.]*}/{path3:[^\\.]*}/{path4:[^\\.]*}/{path5:[^\\.]*}"})Hoover
Another option worked for me. ThanksTrudy
This should be the accepted answer, it's short simple & sweet. Oh and it works :)Nietzsche
A
3
//add this controller : perfect solution(from jhipster)
@Controller
public class ClientForwardController {
    @GetMapping(value = "/**/{path:[^\\.]*}")
    public String forward() {
        return "forward:/";
    }
}
Adhamh answered 4/9, 2019 at 20:16 Comment(5)
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, 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.Thrum
We shoud always try to refrain from using regex at such places as it is vulnerable to ReDOS attacks.Idzik
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 requestsDukey
This throws an error. You can't add patters after wildcards?Panamerican
throws an error with spring boot 2.6 at least: Invalid mapping pattern detected: /**/{path:[^\\.]+} No more pattern data allowed after {*...} or ** pattern elementTilghman
B
2

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.

Brigantine answered 3/4, 2019 at 12:9 Comment(1)
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.Sterling
A
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>
Albigenses answered 13/2, 2019 at 6:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.