I am trying to implement a technique described here: use html5 mode with servlets with webflux.
In a nutshell, users need to be able to refresh a page from their browser without being redirected to the 404
whitelabel page from Spring Boot.
The tutorial above relies on a technique using servlets' forward:
mechanism:
@Controller
public class ForwardController {
@RequestMapping(value = "/**/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
}
However I use webflux and not servlets. Here is what I have tried using a WebFilter
:
@Component
public class SpaWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api") && path.matches("[^\\\\.]*")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/").build()
).build());
}
return chain.filter(exchange);
}
}
When the user refreshes the page, this results in a 404
.
edit: Let me describe the issue in more details:
Once the SPA is loaded in the browser, the user can navigate using the angular route links. Say from http://localhost:8080/
to http://localhost:8080/user-list
(here /user-list
is an angular route. This navigation has no interaction with the backend.
Now when the user - still on the /user-list
route - chooses to refresh the browser page, Spring is going to try to resolve the /user-list
path to a backend handler/router function and this will result in a 404 whitelabel error page served from Spring Boot.
What I want to achieve is that the http://localhost:8080/user-list
page is still displayed to the user when they refresh the browser page.
edit 2: Please note that this refresh issue does not occur on the index page (http://localhost:8080/
) because I have implemented this filter:
@Component
public class IndexWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange.getRequest().getURI().getPath().equals("/")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()
).build()
);
}
return chain.filter(exchange);
}
}
It is obviously not feasible to implement one such filter for each of my Angular routes...
edit 3: Please also note that this issue occurs because the frontend is served as a jar on the backend classpath with the following config:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/");
registry.addResourceHandler("/").addResourceLocations("classpath:/index.html");
}
}
In other words, I don't use a frontend proxy nor a reverse proxy (e.g. nginx)
I have found the solution to my issue. What I was getting wrong was the value of the url "forwarded" to.
By using /index.html
instead of /
, the app behaves as expected.
@Component
public class SpaWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!path.startsWith("/api") && path.matches("[^\\\\.]*")) {
return chain.filter(
exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build()
).build());
}
return chain.filter(exchange);
}
}
The same can be achieved with NGINX as follows:
location / {
try_files $uri $uri/ /index.html;
}
This assumes that the angular routes must not contain any dot and must not start with /api
prefix.
This was the solution I found which works:
https://github.com/emmapatterson/webflux-kotlin-angular/blob/master/README.md
Hope this helps! The main code is:
import org.springframework.context.annotation.Configuration
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
private const val INDEX_FILE_PATH = "/index.html"
@Configuration
internal class StaticContentConfiguration(): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val path = exchange.request.uri.path
if (pathMatchesWebAppResource(path)) {
return redirectToWebApp(exchange, chain)
}
return chain.filter(exchange)
}
private fun redirectToWebApp(exchange: ServerWebExchange, chain: WebFilterChain) =
chain.filter(exchange.mutate()
.request(exchange.request.mutate().path(INDEX_FILE_PATH).build())
.build())
private fun pathMatchesWebAppResource(path: String) =
!path.startsWith("/api") && path.matches("[^\\\\.]*".toRegex())
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With