60

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?

oli
  • 691
  • 1
  • 8
  • 13

9 Answers9

45

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.

Petri Ryhänen
  • 747
  • 1
  • 8
  • 10
  • Thanks! This works great with Angular 4 RouterModule + Spring Boot Embedded Container type setups ;) – Deniss M. Sep 12 '17 at 15:18
  • Work form me too. I have a static angular 4 content in resources and I can reach every route from my UI app! – RazvanParautiu Nov 08 '17 at 15:01
  • images not served – Suroj Mar 18 '19 at 14:49
  • 1
    I got an issue with the third `addViewController` method call. My scenario is that I have webfonts folder that is not served. I think it's because the regex only specifies js and css extensions. I ended up removing the third one and adding a new resource handler. – Lichader Jun 19 '19 at 04:18
  • 10
    Hi, what does the `spring` prefix in the code mean? – hguser Nov 16 '19 at 09:53
  • @hguser It's just a name that's not used here but it's been a while since I worked with spring so I can't remember if it was required. – Petri Ryhänen Nov 18 '19 at 07:35
  • 4
    The regex does not work for hyphens ( - ) nor for underscore ( _ ) . You can replace `\\w` for `^[a-zA-Z\d-_]` to make it work for URLs with those characters – victor.ja Jan 30 '20 at 03:28
  • 2
    @hguser `spring` is a variable name to which the matched part is assigned. However, it is only used here to enable the use of a regex in an AntMatcher (https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html) and does not fill a purpose since it is not used. You can change it to anything else but its purpose is to enable matching with a regex. To use is, redirect or forward with `{spring}` in the path, then you will use whatever was matched. The last entry is ambiguous since the same variable is assigned both matches ... – fast-reflexes Sep 22 '20 at 07:34
  • The second pattern contains the first pattern ('/**/' also matches '/'), so the first pattern is redundant. Feel free to prove me wrong, I don't mind learning something :) – bluemind Feb 17 '21 at 09:53
  • As to the OP's wondering why this prevents infinite loops, you just need to prevent matching /index.html, which would cause a loop. Here it works because the first/second pattern doesn't match anything with an extension (no .html), and the last pattern doesn't match anything in the root (it must start with /something/) – bluemind Feb 17 '21 at 09:55
  • A better regex is to use ^[\\w-] to support urls with alpha, numeric, hyphen, and underscore characters. – Casey Oct 27 '21 at 08:41
  • 1
    How can I do this with PathPatternParser? Spring Boot changed the default to that from AntPathMatcher – F43nd1r Jan 15 '22 at 22:43
32

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.

Dan Syrstad
  • 1,164
  • 11
  • 9
  • 1
    This is not working for me, we have the webapp path into `/resources/static/index.html` but still not working, all i get is a random view from any `RestController` – Yago Rodriguez Jan 21 '19 at 14:52
  • 5
    Thanks, this worked for me - had only to change the resourceLocation to `classpath:/static/` to get it running with my React app. Note that since Spring 5.x.x. extending `WebMvcConfigurerAdapter` is deprecated and `WebMvcConfigurer` has to be implemented instead. – mtsz Mar 02 '20 at 21:51
  • This is cleaner and better explained than the the current top voted answer :) – bluemind Feb 17 '21 at 09:58
  • This works for me but it doesn't handle trailing forward slashes - any suggestions? – sometimes24 Jul 19 '23 at 14:51
12

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: 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

alexbt
  • 16,415
  • 6
  • 78
  • 87
  • 1
    I think this should be marked as answer for your question. – pesoklp13 Sep 26 '17 at 14:01
  • Instead of index.html, I have probe.html in templates folder. If I follow this example, I get: "No handler found for GET /probe" I also tried with index.html, but I get same error. – Angelina Oct 09 '18 at 16:10
  • 8
    This does not solve client-side routing as requested it the question. It serves `index.html` only for root path. It does not redirect URLs like `/campaigns/33` to `index.html`. – Radek Matěj Oct 15 '19 at 11:17
9

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.

user3086678
  • 111
  • 1
  • 5
  • This doesn't work for me. I get "No handler found for GET /index" – Angelina Oct 09 '18 at 16:19
  • Try "index.html" instead of "index" – epsilonmajorquezero Dec 04 '18 at 14:34
  • My current project follow this approach, but we have urls like"/portal/**", "/portal/user/**", "/portal/profile/**". Normal navigation works. But when we refresh on the url, say "/portal/user" I get error Circular view path [index.html]: would dispatch back to the current handler URL. How to handle this, any idea? – this.srivastava Jul 19 '21 at 07:34
2

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, "/"));
        }
    };
}
Community
  • 1
  • 1
oli
  • 691
  • 1
  • 8
  • 13
1

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.

Craigo
  • 3,384
  • 30
  • 22
1

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());
    }
}
Jurass
  • 435
  • 1
  • 5
  • 17
0

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.

Gandalf
  • 71
  • 1
  • 4
  • This works starting from `/` and navigating in the SPA, but is not bookmarkable / and doesn't survive a refresh. – mtsz Mar 02 '20 at 21:48
0

I would like to share a solution based on Jurass answer.

Spring Boot 3.1 + SPA Angular app in the /resources/static folder.

Here is the filter:

private Filter staticResourceFilter() {
    return (request, response, chain) -> {
        String path = ((HttpServletRequest) request).getRequestURI();

        boolean isApi = path.startsWith("/api/v1");
        boolean isStaticResource = path.matches(".*\\.(js|css|ico|html)");

        if (isApi || isStaticResource) {
            chain.doFilter(request, response);
        } else {
            request.getRequestDispatcher("/index.html").forward(request, response);
        }
    };
}

And how it is used in the Spring Security filter chain:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .cors(withDefaults())
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(authorizeConfig -> authorizeConfig
            .requestMatchers("/index.html", "/*.js", "/*.css", "/*.ico",
                "/api/v1/auth/login",
                // others routes...
            ).permitAll()
            .anyRequest().fullyAuthenticated()
        )
        .addFilterBefore(staticResourceFilter(), AuthorizationFilter.class)
        // others security stuff (oauth2, etc.)
    return http.build();
}

All requests that are not API calls or static resources will be forwarded to the /index.html page, so Angular can take over the routing process.

M4veR1K
  • 21
  • 1
  • 9