Spring catch all route for index.html

SpringSpring MvcSpring BootSingle Page-ApplicationCatch All

Spring Problem Overview


I'm developing a spring backend for a react-based single page application where I'm using react-router for client-side routing.

Beside the index.html page the backend serves data on the path /api/**.

In order to serve my index.html from src/main/resources/public/index.html on the root path / of my application I added a resource handler

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

What I want to is to serve the index.html page whenever no other route matches, e.g. when I call a path other than /api.

How do I configure such catch-all route in spring?

Spring Solutions


Solution 1 - Spring

Since my react app could use the root as forward target this ended up working for me

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
      registry.addViewController("/{spring:\\w+}")
            .setViewName("forward:/");
      registry.addViewController("/**/{spring:\\w+}")
            .setViewName("forward:/");
      registry.addViewController("/{spring:\\w+}/**{spring:?!(\\.js|\\.css)$}")
            .setViewName("forward:/");
  }
}

To be honest I have no idea why it has to be exactly in this specific format to avoid infinite forwarding loop.

Solution 2 - Spring

I have a Polymer-based PWA hosted inside of my Spring Boot app, along with static web resources like images, and a REST API under "/api/...". I want the client-side app to handle the URL routing for the PWA. Here's what I use:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    /**
     * Ensure client-side paths redirect to index.html because client handles routing. NOTE: Do NOT use @EnableWebMvc or it will break this.
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // Map "/"
        registry.addViewController("/")
                .setViewName("forward:/index.html");

        // Map "/word", "/word/word", and "/word/word/word" - except for anything starting with "/api/..." or ending with
        // a file extension like ".js" - to index.html. By doing this, the client receives and routes the url. It also
        // allows client-side URLs to be bookmarked.

        // Single directory level - no need to exclude "api"
        registry.addViewController("/{x:[\\w\\-]+}")
                .setViewName("forward:/index.html");
        // Multi-level directory path, need to exclude "api" on the first part of the path
        registry.addViewController("/{x:^(?!api$).*$}/**/{y:[\\w\\-]+}")
                .setViewName("forward:/index.html");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/webapp/");
    }
}

This should work for Angular and React apps as well.

Solution 3 - Spring

Avoid @EnableWebMvc

By default Spring-Boot serves static content in src/main/resources:

  • /META-INF/resources/
  • /resources/
  • /static/
  • /public/

Take a look at this and this;

Or keep @EnableWebMvc and override addViewControllers

Did you specify @EnableWebMvc ? Take a look a this: https://stackoverflow.com/questions/27381781/java-spring-boot-how-to-map-my-app-root-to-index-html

Either you remove @EnableWebMvc, or you can re-define addViewControllers:

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

Or define a Controller to catch /

You may take a look a this spring-boot-reactjs sample project on github:

It does what you want using a Controller:

@Controller
public class HomeController {

	@RequestMapping(value = "/")
	public String index() {
		return "index";
	}

}

Its index.html is under src/main/resources/templates

Solution 4 - Spring

I use react and react-router in my spring boot app, and it was as easy as creating a controller that has mapping to / and subtrees of my website like /users/** Here is my solution

@Controller
public class SinglePageAppController {
    @RequestMapping(value = {"/", "/users/**", "/campaigns/**"})
    public String index() {
        return "index";
    }
}

Api calls aren't caught by this controller and resources are handled automatically.

Solution 5 - Spring

Found an answer by looking at this question

@Bean
public EmbeddedServletContainerCustomizer notFoundCustomizer() {
    return new EmbeddedServletContainerCustomizer() {
        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
        }
    };
}

Solution 6 - Spring

Another solution (change/add/remove myurl1, myurl2, ... with your routes):

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

import javax.servlet.http.HttpServletRequest;

@Controller
public class SinglePageAppController {

    /**
     * If the user refreshes the page while on a React route, the request will come here.
     * We need to tell it that there isn't any special page, just keep using React, by
     * forwarding it back to the root.
     */
    @RequestMapping({"/myurl1/**", "/myurl2/**"})
    public String forward(HttpServletRequest httpServletRequest) {
        return "forward:/";
    }
}

Note: Using public String index() also works fine, but only if you use templates. And the use of WebMvcConfigurerAdapter is deprecated.

Solution 7 - Spring

To answer your specific question which involves serving up the Single Page App (SPA) in all cases except the /api route here is what I did to modify Petri's answer.

I have a template named polymer that contains the index.html for my SPA. So the challenge became let's forward all routes except /api and /public-api to that view.

In my WebMvcConfigurerAdapter I override addViewControllers and used the regular expression: ^((?!/api/|/public-api/).)*$

In your case you want the regular expression: ^((?!/api/).)*$

public class WebMvcConfiguration extends WebMvcConfigurerAdapter {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
	registry.addViewController("/{spring:^((?!/api/).)*$}").setViewName("polymer");
	super.addViewControllers(registry);
}

This results in being able to hit http://localhost or http://localhost/community to serve up my SPA and all of the rest calls that the SPA makes being successfully routed to http://localhost/api/posts, http://localhost/public-api/posts, etc.

Solution 8 - Spring

After lot of tries I've found the following solution as most simple one. It will basically bypass all the Spring handling which was so difficult to deal with.

@Component
public class StaticContentFilter implements Filter {
    
    private List<String> fileExtensions = Arrays.asList("html", "js", "json", "csv", "css", "png", "svg", "eot", "ttf", "woff", "appcache", "jpg", "jpeg", "gif", "ico");
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }
    
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String path = request.getServletPath();
        
        boolean isApi = path.startsWith("/api");
        boolean isResourceFile = !isApi && fileExtensions.stream().anyMatch(path::contains);
        
        if (isApi) {
            chain.doFilter(request, response);
        } else if (isResourceFile) {
            resourceToResponse("static" + path, response);
        } else {
            resourceToResponse("static/index.html", response);
        }
    }
    
    private void resourceToResponse(String resourcePath, HttpServletResponse response) throws IOException {
        InputStream inputStream = Thread.currentThread()
                .getContextClassLoader()
                .getResourceAsStream(resourcePath);
        
        if (inputStream == null) {
            response.sendError(NOT_FOUND.value(), NOT_FOUND.getReasonPhrase());
            return;
        }
        
        inputStream.transferTo(response.getOutputStream());
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionoliView Question on Stackoverflow
Solution 1 - SpringPetri RyhänenView Answer on Stackoverflow
Solution 2 - SpringDan SyrstadView Answer on Stackoverflow
Solution 3 - SpringalexbtView Answer on Stackoverflow
Solution 4 - Springuser3086678View Answer on Stackoverflow
Solution 5 - SpringoliView Answer on Stackoverflow
Solution 6 - SpringCraigoView Answer on Stackoverflow
Solution 7 - SpringGandalfView Answer on Stackoverflow
Solution 8 - SpringJurassView Answer on Stackoverflow