feat(ext-vite): recognise navigations by Sec-Fetch-Mode, read every header in place

A path that is no file falls back to index.html when the request is a navigation:
Sec-Fetch-Mode: navigate, or an Accept naming text/html for older clients. That check,
Accept-Encoding and If-None-Match are all matched on the header bytes through the new
Request.headerView(name), so serving still allocates nothing. navigationOnly(false)
drops the check for an app that wants every GET miss to get the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-09-22 16:16:27 +00:00
co-authored by Claude Opus 5
parent 680bbca8c7
commit 003fd6d1f0
6 changed files with 116 additions and 29 deletions
@@ -1,5 +1,6 @@
package dev.relism.flash.ext.vite;
import dev.relism.fpr.core.ByteView;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -33,24 +34,41 @@ class AssetsTest {
}
}
try (URLClassLoader loader = new URLClassLoader(new URL[]{jar.toUri().toURL()}, null)) {
assertEquals(Set.of("/", "/index.html", "/assets/a-12345678.css"), new Assets(loader, "/").byPath.keySet());
assertEquals(Set.of("/", "/index.html", "/assets/a-12345678.css"), new Assets(loader, "/", true).byPath.keySet());
}
}
@Test
void noBuildFailsTheBootNamingThePlugin() throws IOException {
try (URLClassLoader empty = new URLClassLoader(new URL[0], null)) {
assertTrue(assertThrows(IllegalStateException.class, () -> new Assets(empty, "/")).getMessage().contains("flash-ext-vite-maven-plugin"));
assertTrue(assertThrows(IllegalStateException.class, () -> new Assets(empty, "/", true)).getMessage().contains("flash-ext-vite-maven-plugin"));
}
}
@Test
void gzipIsAcceptedUnlessRefused() {
assertTrue(Assets.acceptsGzip("gzip, deflate, br"));
assertTrue(Assets.acceptsGzip("br;q=1.0, gzip;q=0.5"));
assertFalse(Assets.acceptsGzip("gzip;q=0, br"));
assertFalse(Assets.acceptsGzip("gzip;q=0.000"));
assertFalse(Assets.acceptsGzip("br"));
assertTrue(Assets.acceptsGzip(view("gzip, deflate, br")));
assertTrue(Assets.acceptsGzip(view("br;q=1.0, GZIP;q=0.5")));
assertFalse(Assets.acceptsGzip(view("gzip;q=0, br")));
assertFalse(Assets.acceptsGzip(view("gzip;q=0.000")));
assertFalse(Assets.acceptsGzip(view("br")));
assertFalse(Assets.acceptsGzip(null));
}
@Test
void searchingIgnoresAsciiCaseAndFindsTheFirstMatch() {
byte[] html = "text/html".getBytes();
assertEquals(0, Assets.indexOf(view("Text/HTML,*/*"), html, 0));
assertEquals(12, Assets.indexOf(view("application/text/html"), html, 0));
assertEquals(-1, Assets.indexOf(view("text/htm"), html, 0));
assertEquals(-1, Assets.indexOf(null, html, 0));
}
private static ByteView view(String text) {
byte[] bytes = text.getBytes();
return new ByteView() {
@Override public int length() { return bytes.length; }
@Override public byte byteAt(int i) { return bytes[i]; }
};
}
}
@@ -16,6 +16,9 @@ class ViteExtensionTest {
@RegisterExtension
static final FlashTest app = FlashTest.of(flash -> flash.install(new ViteExtension()));
@RegisterExtension
static final FlashTest everything = FlashTest.of(flash -> flash.install(new ViteExtension().navigationOnly(false)));
@RegisterExtension
static final FlashTest nested = FlashTest.of(flash -> flash.install(new ViteExtension().basePath("/app/")));
@@ -41,6 +44,7 @@ class ViteExtensionTest {
for (String route : new String[]{"/content/2", "/users/ada.lovelace"}) {
app.request().header("Accept", NAVIGATION).get(route).expectStatus(200).expectHeader("Cache-Control", "no-cache").expectBodyContains("spa");
}
app.request().header("Sec-Fetch-Mode", "navigate").get("/content/2").expectStatus(200).expectBodyContains("spa");
app.request().header("Accept", "application/json").get("/api/nope").expectStatus(404);
app.request().header("Accept", "*/*").get("/assets/missing.js").expectStatus(404);
app.get("/content/2").expectStatus(404);
@@ -48,6 +52,13 @@ class ViteExtensionTest {
nested.get("/app").expectStatus(200).expectBodyContains("spa");
}
/** Opted out of the navigation check, every GET that is no file gets the page, as nginx's try_files would. */
@Test
void withoutTheNavigationCheckEveryMissGetsTheIndex() {
everything.request().header("Accept", "application/json").get("/api/nope").expectStatus(200).expectBodyContains("spa");
everything.get("/assets/missing.js").expectStatus(200).expectBodyContains("spa");
}
@Test
void anUnchangedFileIsNotSentAgainAHashedOneIsNeverAsked() {
String etag = app.get("/favicon.svg").header("ETag");