Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle a page refresh using html 5 mode & Spring webflux

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)

like image 609
balteo Avatar asked Aug 21 '19 07:08

balteo


Video Answer


2 Answers

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.

like image 140
balteo Avatar answered Sep 17 '22 16:09

balteo


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())
}
like image 43
Emma Patterson Avatar answered Sep 17 '22 16:09

Emma Patterson