From 3a91d90c28ec2012cbda2f7fa39910a1ea4cc31f Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 30 Apr 2026 15:17:45 +0200 Subject: [PATCH] Resolve URL path for versioned webjar directories Prior to this commit, the `resolveUrlPath` implementation for the `LiteWebJarsResourceResolver` would always delegate to the resource chain once the versioned webjar folder has been resolved. While this aligns with the `ResourceResolver` contract and the fact that the resource chain does not resolve directories, here the WebJar locator does support such use cases and we shouldn't get in the way here. This commit falls back to the resolved versioned WebJar path if no resource could be resolved and the path ends with "/". Fixes gh-36726 --- framework-platform/framework-platform.gradle | 1 - spring-webflux/spring-webflux.gradle | 2 +- .../resource/LiteWebJarsResourceResolver.java | 3 +- .../LiteWebJarsResourceResolverTests.java | 28 +++++++++++++++---- spring-webmvc/spring-webmvc.gradle | 2 +- .../resource/LiteWebJarsResourceResolver.java | 5 +++- .../LiteWebJarsResourceResolverTests.java | 28 +++++++++++++++---- 7 files changed, 52 insertions(+), 17 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 986fed60b25..8fce3ed3215 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -138,7 +138,6 @@ dependencies { api("org.seleniumhq.selenium:selenium-java:4.41.0") api("org.skyscreamer:jsonassert:1.5.3") api("org.testng:testng:7.12.0") - api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-lite:1.1.0") api("org.xmlunit:xmlunit-assertj:2.10.4") api("org.xmlunit:xmlunit-matchers:2.10.4") diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 16c9be3a271..8de09951ee5 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -61,7 +61,7 @@ dependencies { testRuntimeOnly("org.glassfish:jakarta.el") testRuntimeOnly("org.jruby:jruby") testRuntimeOnly("org.python:jython-standalone") - testRuntimeOnly("org.webjars:underscorejs") + testRuntimeOnly("org.webjars:momentjs:2.29.4") } test { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java index 6983d2fddfa..3fd7b40c402 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolver.java @@ -90,7 +90,8 @@ public class LiteWebJarsResourceResolver extends AbstractResourceResolver { .switchIfEmpty(Mono.defer(() -> { String webJarResourcePath = findWebJarResourcePath(resourceUrlPath); if (webJarResourcePath != null) { - return chain.resolveUrlPath(webJarResourcePath, locations); + Mono fallback = (webJarResourcePath.endsWith("/")) ? Mono.just(webJarResourcePath) : Mono.empty(); + return chain.resolveUrlPath(webJarResourcePath, locations).switchIfEmpty(fallback); } else { return Mono.empty(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java index e7a5ce16cff..fe8d35f87c9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/LiteWebJarsResourceResolverTests.java @@ -29,6 +29,8 @@ import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRe import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -80,8 +82,8 @@ class LiteWebJarsResourceResolverTests { @Test void resolveUrlWebJarResource() { - String file = "underscorejs/underscore.js"; - String expected = "underscorejs/1.8.3/underscore.js"; + String file = "momentjs/momentjs.js"; + String expected = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty()); given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.just(expected)); @@ -92,10 +94,24 @@ class LiteWebJarsResourceResolverTests { verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); } + @Test + void resolveUrlWebJarDirectory() { + String folder = "momentjs/locale/"; + String expected = "momentjs/2.29.4/locale/"; + given(this.chain.resolveUrlPath(folder, this.locations)).willReturn(Mono.empty()); + given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.empty()); + + String actual = this.resolver.resolveUrlPath(folder, this.locations, this.chain).block(TIMEOUT); + + assertThat(actual).isEqualTo(expected); + verify(this.chain, times(1)).resolveUrlPath(folder, this.locations); + verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); + } + @Test void resolveUrlWebJarResourceNotFound() { - String file = "something/something.js"; - given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty()); + String file = "momentjs/locale/unknown.js"; + given(this.chain.resolveUrlPath(anyString(), eq(this.locations))).willReturn(Mono.empty()); String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT); @@ -134,11 +150,11 @@ class LiteWebJarsResourceResolverTests { @Test void resolveResourceWebJar() { - String file = "underscorejs/underscore.js"; + String file = "momentjs/momentjs.js"; given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.empty()); Resource expected = mock(); - String expectedPath = "underscorejs/1.8.3/underscore.js"; + String expectedPath = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveResource(this.exchange, expectedPath, this.locations)) .willReturn(Mono.just(expected)); diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 760530e3c87..194278ad7c0 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -80,5 +80,5 @@ dependencies { testRuntimeOnly("org.glassfish:jakarta.el") testRuntimeOnly("org.jruby:jruby") testRuntimeOnly("org.python:jython-standalone") - testRuntimeOnly("org.webjars:underscorejs") + testRuntimeOnly("org.webjars:momentjs:2.29.4") } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java index 1486063b0e2..358927ab1e7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolver.java @@ -88,7 +88,10 @@ public class LiteWebJarsResourceResolver extends AbstractResourceResolver { if (path == null) { String webJarResourcePath = findWebJarResourcePath(resourceUrlPath); if (webJarResourcePath != null) { - return chain.resolveUrlPath(webJarResourcePath, locations); + path = chain.resolveUrlPath(webJarResourcePath, locations); + if (path == null && webJarResourcePath.endsWith("/")) { + path = webJarResourcePath; + } } } return path; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java index 101c0630692..724ab79d87a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LiteWebJarsResourceResolverTests.java @@ -26,6 +26,8 @@ import org.springframework.core.io.Resource; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -74,8 +76,8 @@ class LiteWebJarsResourceResolverTests { @Test void resolveUrlWebJarResource() { - String file = "underscorejs/underscore.js"; - String expected = "underscorejs/1.8.3/underscore.js"; + String file = "momentjs/momentjs.js"; + String expected = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null); given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(expected); @@ -86,10 +88,24 @@ class LiteWebJarsResourceResolverTests { verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); } + @Test + void resolveUrlWebJarDirectory() { + String folder = "momentjs/locale/"; + String expected = "momentjs/2.29.4/locale/"; + given(this.chain.resolveUrlPath(folder, this.locations)).willReturn(null); + given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(null); + + String actual = this.resolver.resolveUrlPath(folder, this.locations, this.chain); + + assertThat(actual).isEqualTo(expected); + verify(this.chain, times(1)).resolveUrlPath(folder, this.locations); + verify(this.chain, times(1)).resolveUrlPath(expected, this.locations); + } + @Test void resolveUrlWebJarResourceNotFound() { - String file = "something/something.js"; - given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null); + String file = "momentjs/locale/unknown.js"; + given(this.chain.resolveUrlPath(anyString(), eq(this.locations))).willReturn(null); String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain); @@ -125,8 +141,8 @@ class LiteWebJarsResourceResolverTests { @Test void resolveResourceWebJar() { Resource expected = mock(); - String file = "underscorejs/underscore.js"; - String expectedPath = "underscorejs/1.8.3/underscore.js"; + String file = "momentjs/momentjs.js"; + String expectedPath = "momentjs/2.29.4/momentjs.js"; given(this.chain.resolveResource(this.request, expectedPath, this.locations)).willReturn(expected); Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);