staticFiles: a dot in a path segment is not a file extension

The route ladder was gated on looksLikeAsset, which decides from path.Ext. That
predicate is right about "/chunk-abc123.js" and wrong about a URL segment that
merely contains a dot: "/models/z-ai/glm-5.2" has extension ".2", and
"/models/meta-llama/llama-3.2-1b-instruct" has ".2-1b-instruct". Both are pages.
Measured live on hanzo.ai: 181 of 773 routes 404d — every model page carrying a
version number — while the dotless ones served.

The gate is removed rather than made cleverer. No pattern reliably separates a
page from a file by name, and the ladder never needed it: it asks for ONE exact
file per candidate and never falls back to a shell, so it has nothing to mask
anything with. A missing chunk looks for "nope.js.html" and "nope.js/index.html",
finds neither, and is still a bare 404 — now guaranteed by construction instead
of by a guess.

looksLikeAsset keeps gating SPA mode, which is where the risk actually lives:
that path answers with the shell for anything, so it is the one that must never
answer an asset request.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-05 16:58:11 -07:00
co-authored by Hanzo Dev
parent 548bf285a6
commit 5301c301c0
2 changed files with 50 additions and 11 deletions
+28 -1
View File
@@ -20,7 +20,7 @@ func exportRoot(t *testing.T) string {
t.Helper()
dir := t.TempDir()
for _, sub := range []string{"docs", "blog", "_next/static/chunks"} {
for _, sub := range []string{"docs", "blog", "models", "_next/static/chunks"} {
if err := os.MkdirAll(filepath.Join(dir, sub), 0o755); err != nil {
t.Fatal(err)
}
@@ -28,6 +28,8 @@ func exportRoot(t *testing.T) string {
files := map[string]string{
"index.html": "<!doctype html><title>home</title>",
"models/glm-5.2.html": "<!doctype html><title>glm</title>",
"models/llama-3.2-1b.html": "<!doctype html><title>llama</title>",
"pricing.html": "<!doctype html><title>pricing</title>",
"docs/index.html": "<!doctype html><title>docs</title>",
"blog.html": "<!doctype html><title>blog</title>",
@@ -133,6 +135,31 @@ func TestFileBackedRouteDoesNotRedirect(t *testing.T) {
}
}
// TestDottedRouteSegmentIsAPage is the bug the first cut of this shipped with.
// A version number in the last segment makes path.Ext non-empty — ".2" for
// "glm-5.2", ".2-1b" for "llama-3.2-1b" — so an extension-based "is this an
// asset" guess treats a page as a file and skips the ladder. On hanzo.ai that
// was 181 of 773 routes: every model page carrying a version, 404 while the
// plain routes served.
func TestDottedRouteSegmentIsAPage(t *testing.T) {
srv := httptest.NewServer(newLocal(t, dynamic.StaticFiles{Root: exportRoot(t)}))
defer srv.Close()
for path, want := range map[string]string{
"/models/glm-5.2": "<title>glm</title>",
"/models/llama-3.2-1b": "<title>llama</title>",
} {
status, _, body := get(t, srv, path)
if status != http.StatusOK {
t.Errorf("GET %s = %d, want 200 — a dot in a segment is not an extension", path, status)
continue
}
if !strings.Contains(body, want) {
t.Errorf("GET %s served %q, want %q", path, truncate(body), want)
}
}
}
// TestRootServesIndexWithoutRedirect pins that "/" is answered, not moved. The
// index loop used to 301 to "/index.html", which relocates every site's
// canonical URL onto a path nothing links to.
+22 -10
View File
@@ -198,16 +198,28 @@ func (h *staticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
if os.IsNotExist(err) {
// A prerendered route before any fallback policy: the file exists,
// it is just named "pricing.html" rather than "pricing". Gated on
// looksLikeAsset so a missing content-hashed chunk stays a bare 404
// and can never be answered with a page.
if !looksLikeAsset(upath) {
for _, candidate := range pageCandidates(upath) {
if cf, cd, ok := h.openRegular(r.Context(), candidate); ok {
defer cf.Close()
h.serveOpen(w, r, cf, cd)
return
}
// it is just named "pricing.html" rather than "pricing".
//
// Deliberately NOT gated on looksLikeAsset. That predicate guesses
// from the extension, and a URL segment may carry a dot without
// being a file: "/models/z-ai/glm-5.2" has path.Ext ".2" and
// "/models/meta-llama/llama-3.2-1b-instruct" has ".2-1b-instruct".
// Gating here 404d 181 of hanzo.ai's 773 routes — every model page
// with a version number in it — while the plain ones worked.
//
// It does not need the gate. The ladder asks for ONE exact file per
// candidate; it never falls back to a shell, so there is nothing for
// it to mask something with. A missing chunk looks for
// "nope.js.html" and "nope.js/index.html", finds neither, and stays
// a bare 404 — the guarantee holds by construction rather than by
// guessing what a path means. looksLikeAsset still gates SPA mode
// below, which is where it belongs: THAT path serves the shell for
// anything, so it is the one that must not answer an asset.
for _, candidate := range pageCandidates(upath) {
if cf, cd, ok := h.openRegular(r.Context(), candidate); ok {
defer cf.Close()
h.serveOpen(w, r, cf, cd)
return
}
}