8

I'm currently working on a webpage that should use a single page React front-end. For the back-end, I'm using the spring boot framework.

All api calls shall use a url prefixed with /api and should be handled by the REST controllers.

All other urls should simply serve the index.html file. How would I achieve this with spring?

Maciej Marczuk
  • 3,593
  • 29
  • 28
irundaia
  • 1,720
  • 17
  • 25

2 Answers2

10

The easiest way to achieve what you want is to implement custom 404 handler.

Add these params to your application.properties:

spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true

First property removes all default static resource handling, second property disables Spring's default whitelabel page (by default Spring catches NoHandlerFoundException and serves standard whitelabel page)

Add 404 handler to your application context:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class PageNotFoundController {
    @ExceptionHandler(NoHandlerFoundException.class)
    public String handleError404() {
            return "redirect:/index.html";
    }
}

At the end you will need to add your custom view resolver for serving your static content (index.html in this case)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/index.html").addResourceLocations("classpath:/static/index.html");
        super.addResourceHandlers(registry);
    }

    @Bean
    public ViewResolver viewResolver() {
        UrlBasedViewResolver viewResolver = new UrlBasedViewResolver();
        viewResolver.setViewClass(InternalResourceView.class);
        return viewResolver;
    }

}

Your index.html should be placed in /resources/static/ directory.

Maciej Marczuk
  • 3,593
  • 29
  • 28
  • 2
    What if there's SpringSecurity present in the app ? In that case `NoHandlerFoundException` doesn't even get fired. Server just throws `InsufficientAuthenticationException`. And when I simply replaced the `PageNotFoundController`'s `@ExceptionHandler` annotation with that exception, it didn't seem to work. Server returned a 401 Unauthorized white label page. Do you have a suggestion for that ? – alegria Jun 01 '18 at 07:04
1

Please make sure to include thymeleaf in pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Add these in your application.properties

spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER
spring.thymeleaf.check-template-location=true

The following is optional. You might need this, for example, if you want to change the path to store static files.

spring.web.resources.static-locations=classpath:/your/path/to/the/*folder*/that/has/index/file
spring.thymeleaf.prefix=classpath:/your/path/to/the/*folder*/that/has/index/file

Create your SpaController.java then write like this:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SpaController {
    @RequestMapping(value = {"/{path:^(?!api|public)[^\\.]*}", "/**/{path:^(?!api|public).*}/{path:[^\\.]*}"})
    public String get(){
        return "index";
    }
}

Or, in Kotlin:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SpaController {
    @RequestMapping(value = [
        "/{path:^(?!api|public)[^\\.]*}",
        "/**/{path:^(?!api|public).*}/{path:[^\\.]*}"
    ])
    public fun get() : String
    {
        return "index";
    }
}

But please note that this might not work for path like /***/***/*** (having slashes more than 2). If you need that case, you need to add or modify the matcher.

See also: Spring catch all route for index.html

lechat
  • 390
  • 2
  • 15