Reputation: 691
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?
Upvotes: 60
Views: 47295
Reputation: 73
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.
Upvotes: 1
Reputation: 596
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());
}
}
Upvotes: 1
Reputation: 3717
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.
Upvotes: 1
Reputation: 1447
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.
Upvotes: 32
Reputation: 81
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.
Upvotes: 0
Reputation: 17025
Avoid @EnableWebMvc
By default Spring-Boot serves static content in src/main/resources
:
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
Upvotes: 12
Reputation: 797
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.
Upvotes: 47
Reputation: 111
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.
Upvotes: 9
Reputation: 691
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, "/"));
}
};
}
Upvotes: 2