Michael Gmeiner opened SPR-16788 and commented
I'm currently migrating a spring-boot app to spring-boot 2 and webflux. Everything looks great and webflux is an amazing integration of Project Reactor into the spring ecosystem. So first thanks for the great work!
Our app serves a SPA which takes use of the History-API, which means there are urls they can only be accessed via a client side router and do not have a corresponding resource in the serving app. That means it should return the index for every resource which cannot be found on the server because the client side router takes care of that. Before Webflux I implemented this with customizing the WebMvcConfigurer like this:
@Configuration
class WebMvcConfig @Autowired constructor(val resourceProperties: ResourceProperties): WebMvcConfigurer {
override fun addResourceHandlers( registry: ResourceHandlerRegistry ) {
registry.addResourceHandler( "/**" )
.addResourceLocations( *resourceProperties.staticLocations )
.setCachePeriod( 0 ) // no cache
.resourceChain( resourceProperties.chain.isCache )
.addResolver(object: PathResourceResolver() {
override fun resolveResource(request: HttpServletRequest?, requestPath: String, locations: MutableList<out Resource>, chain: ResourceResolverChain): Resource {
return super.resolveResource(request, requestPath, locations, chain) ?: super.resolveResource( request, "/index.html", locations, chain )
}
})
}
}
In Webflux this could be also possbile when i register my own lookup function for resources which just overrides the
org.springframework.web.reactive.function.server.PathResourceLookupFunction
But that is not possible because the PathResourceLookupFunction class is package private.
My question is now if it would be possible to make the PathResourceLookupFunction class public and make it easy to add something like a fallback resource for it?
Or: provide a function on RouterFunctions to create resource mappings with fallback option:
For example like:
RouterFunctions.resourcesWithFallback("/**", ClassPathResource("static/"), ClassPathResource("static/index.html"))
Please let me know your thoughts about that or if you have any other suggestions to achieve that. If you're ok with some possible solution i described above i could also provide a PR for this.
Thanks in advance
Affects: 5.0.5
Referenced from: commits https://github.com/spring-projects/spring-framework/commit/bfb2effddb7d2cb9b50c1088b3971028633ad8f2
Comment From: spring-projects-issues
Arjen Poutsma commented
Fixed in https://github.com/spring-projects/spring-framework/commit/bfb2effddb7d2cb9b50c1088b3971028633ad8f2
I did not make the PathResourceLookupFunction public, for two reasons:
* All other non-utility classes in the package are package protected, so exposing this class would make it the odd one out.
* Keeping the class non-public allows us to change it without breaking extending classes.
Instead, we now expose the resource router function in RouterFunctions.resourceLookupFunction(String, Resource), returning a Function<ServerRequest, Mono<Resource>> that can be composed upon. This allows for a functional solution to the problem, rather than one based on inheritance.
For instance:
Mono<Resource> defaultResource = Mono.just(new ClassPathResource("index.html"));
Function<ServerRequest, Mono<Resource>> lookupFunction =
RouterFunctions.resourceLookupFunction("/resources/**", new FileSystemResource("public-resources/"))
.andThen(resourceMono -> resourceMono.switchIfEmpty(defaultResource));
RouterFunction<ServerResponse> resources = RouterFunctions.resources(lookupFunction);
Comment From: spring-projects-issues
Michael Gmeiner commented
Thanks for the fast solution!
Yes i understand why making it just public is not a good solution but the one you just implemented is great! Thanks again for the great work!
Comment From: danbim
@poutsma I've run into exactly the same issue as Michael Gmeiner here before and adopted your solution for my application. Delivery of the defaultResource (and all other resources from my React-generated build directory) works just fine.
However, with this configuration all requests are mapped to this RouterFunction, even the ones for e.g. /actuator/mappings/ or my /api/graphql/ endpoint (created by [1]). Is there a way I can make route configurations coexist, e.g. by configuring a precedence over routes?
[1] https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/RoutesConfiguration.kt