mirror of
https://github.com/hanzoai/ingress.git
synced 2026-10-20 15:07:40 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user