Skip to content

Commit a26aa80

Browse files
authored
fix: wrong existence check in getStaticResource (#24283)
On Jetty 12.1.9, requests for static resources packaged inside a JAR (e.g. `vaadinPush.js` from `flow-push`) fail with `FileSystemNotFoundException`. `VaadinServletService.getStaticResource` verifies the URL returned by `ServletContext.getResource` via `Path.of(url.toURI())`, which for a `jar:file:...!/entry` URI requires the JAR's NIO `FileSystem` to already be mounted in the JVM-wide cache. Jetty 12.1.8 incidentally kept those filesystems mounted during resource resolution; 12.1.9 no longer does, so `getFileSystem` throws and the existing `catch (URISyntaxException)` lets the unchecked exception escape, producing HTTP 500. Probe the URL with `URL.openStream()` instead. `JarURLConnection` and `FileURLConnection` use `java.util.jar.JarFile` / `java.io.File` directly and are independent of the NIO `FileSystems` cache, so the check works uniformly for `file:` and `jar:file:` URLs and on every Jetty 12 build. The catch is broadened to `IOException`, covering both missing files (the original Jetty 12 workaround) and missing JAR entries.
1 parent 00b04b9 commit a26aa80

2 files changed

Lines changed: 56 additions & 10 deletions

File tree

flow-server/src/main/java/com/vaadin/flow/server/VaadinServletService.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@
1919
import jakarta.servlet.ServletContext;
2020
import jakarta.servlet.http.HttpServletRequest;
2121

22+
import java.io.IOException;
2223
import java.io.InputStream;
2324
import java.net.MalformedURLException;
24-
import java.net.URISyntaxException;
2525
import java.net.URL;
26-
import java.nio.file.Files;
27-
import java.nio.file.Path;
2826
import java.util.List;
2927
import java.util.Objects;
3028
import java.util.Optional;
@@ -338,13 +336,15 @@ static URL getStaticResource(ServletContext servletContext, String path)
338336
URL url = servletContext.getResource(path);
339337
if (url != null && Optional.ofNullable(servletContext.getServerInfo())
340338
.orElse("").contains("jetty/12.")) {
341-
// Making sure that resource exists before returning it. Jetty
342-
// 12 may return URL for non-existing resource.
343-
try {
344-
if (!Files.exists(Path.of(url.toURI()))) {
345-
url = null;
346-
}
347-
} catch (URISyntaxException e) {
339+
// Jetty 12 may return URLs for non-existing resources. Probe the
340+
// URL by opening a stream: this works uniformly for file: URLs
341+
// and for jar:file:...!/entry URLs without requiring a NIO
342+
// FileSystem to be mounted for the JAR (which Path.of(jarUri)
343+
// would need, breaking on Jetty 12.1.9 where classpath JARs are
344+
// no longer kept mounted in the JVM-wide cache).
345+
try (InputStream probe = url.openStream()) {
346+
// resource exists
347+
} catch (IOException e) {
348348
url = null;
349349
}
350350
}

flow-server/src/test/java/com/vaadin/flow/server/VaadinServletServiceTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,24 @@
2121

2222
import java.io.IOException;
2323
import java.net.MalformedURLException;
24+
import java.net.URI;
2425
import java.net.URL;
26+
import java.nio.charset.StandardCharsets;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
2529
import java.util.Collections;
2630
import java.util.HashMap;
2731
import java.util.List;
2832
import java.util.Map;
33+
import java.util.UUID;
2934
import java.util.concurrent.locks.ReentrantLock;
35+
import java.util.jar.JarEntry;
36+
import java.util.jar.JarOutputStream;
3037

3138
import org.junit.jupiter.api.AfterEach;
3239
import org.junit.jupiter.api.BeforeEach;
3340
import org.junit.jupiter.api.Test;
41+
import org.junit.jupiter.api.io.TempDir;
3442
import org.mockito.Mockito;
3543

3644
import com.vaadin.flow.di.Instantiator;
@@ -331,6 +339,44 @@ protected List<VaadinRequestInterceptor> createVaadinRequestInterceptors()
331339
"Filter was called in the finally block");
332340
}
333341

342+
@Test
343+
void getStaticResource_jarUrlOnJetty12_returnsUrlInsteadOfThrowing(
344+
@TempDir Path tempDir) throws Exception {
345+
// Reproduces the regression caused by Jetty 12.1.9 where
346+
// ServletContext.getResource() returns a jar:file:... URL for
347+
// resources packaged inside a JAR (e.g. vaadinPush.js in flow-push).
348+
// Path.of(jar:URI) throws FileSystemNotFoundException because no
349+
// FileSystem has been opened for the JAR, and the existence check in
350+
// VaadinServletService.getStaticResource catches only
351+
// URISyntaxException, so the unchecked exception escapes and the
352+
// request fails with HTTP 500.
353+
String resourcePath = "META-INF/resources/VAADIN/static/push/vaadinPush.js";
354+
Path jarFile = tempDir
355+
.resolve("flow-push-" + UUID.randomUUID() + ".jar");
356+
try (JarOutputStream jar = new JarOutputStream(
357+
Files.newOutputStream(jarFile))) {
358+
jar.putNextEntry(new JarEntry(resourcePath));
359+
jar.write("// push".getBytes(StandardCharsets.UTF_8));
360+
jar.closeEntry();
361+
}
362+
363+
URL jarResourceUrl = URI
364+
.create("jar:" + jarFile.toUri() + "!/" + resourcePath).toURL();
365+
366+
ServletContext servletContext = Mockito.mock(ServletContext.class);
367+
when(servletContext.getServerInfo()).thenReturn("jetty/12.1.9");
368+
when(servletContext.getResource("/VAADIN/static/push/vaadinPush.js"))
369+
.thenReturn(jarResourceUrl);
370+
371+
URL resolved = VaadinServletService.getStaticResource(servletContext,
372+
"/VAADIN/static/push/vaadinPush.js");
373+
374+
assertNotNull(resolved,
375+
"An existing resource served from a JAR by Jetty 12 must be returned, "
376+
+ "not lost to FileSystemNotFoundException");
377+
assertEquals(jarResourceUrl, resolved);
378+
}
379+
334380
static class ExceptionThrowingRequestHandler implements RequestHandler {
335381

336382
@Override

0 commit comments

Comments
 (0)