Spring catch all route for index.html
Asked Answered
E

9

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?

Elisa answered 5/9, 2016 at 13:42 Comment(0)
I
47

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.

Indiraindirect answered 24/3, 2017 at 11:51 Comment(12)
Thanks! This works great with Angular 4 RouterModule + Spring Boot Embedded Container type setups ;)Skirt
Work form me too. I have a static angular 4 content in resources and I can reach every route from my UI app!Apyretic
images not servedCatacaustic
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.Peace
Hi, what does the spring prefix in the code mean?Maiden
@Maiden 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.Tyrelltyrian
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 charactersJonijonie
@Maiden 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 (docs.spring.io/spring-framework/docs/current/javadoc-api/org/…) 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 ...Homogenous
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 :)Clardy
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/)Clardy
A better regex is to use ^[\\w-] to support urls with alpha, numeric, hyphen, and underscore characters.Arequipa
How can I do this with PathPatternParser? Spring Boot changed the default to that from AntPathMatcherArdath
P
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.

Pyramidon answered 5/6, 2018 at 22:26 Comment(4)
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 RestControllerPaulapauldron
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.Crucifixion
This is cleaner and better explained than the the current top voted answer :)Clardy
This works for me but it doesn't handle trailing forward slashes - any suggestions?Kier
P
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

Petuu answered 5/9, 2016 at 20:12 Comment(3)
I think this should be marked as answer for your question.Loosejointed
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.Urga
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.Toothless
C
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.

Carnarvon answered 4/11, 2016 at 13:57 Comment(3)
This doesn't work for me. I get "No handler found for GET /index"Urga
Try "index.html" instead of "index"Yerxa
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?Akiko
E
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, "/"));
        }
    };
}
Elisa answered 6/9, 2016 at 15:4 Comment(0)
V
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.

Vivienne answered 16/12, 2020 at 23:19 Comment(0)
W
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());
    }
}
Wylma answered 2/7, 2021 at 13:25 Comment(0)
J
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.

Jaconet answered 1/6, 2018 at 17:10 Comment(1)
This works starting from / and navigating in the SPA, but is not bookmarkable / and doesn't survive a refresh.Crucifixion
H
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.

Helmer answered 8/8, 2023 at 13:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.