From 20864ca01fcff65c117620667a405fb18a51f1d9 Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:24:22 +0530 Subject: [PATCH] fix(backend): return 404 instead of panicking on unresolvable static paths serveFilesHandler stats the requested file and only handled os.IsNotExist errors. Every other error left fileInfo nil, so the following fileInfo.IsDir() call panicked. LoggingMiddleware recovered the panic and answered 500 while logging a full stack trace. Ordinary bad URLs hit this path: - /index.html/foo: a path component is a regular file, so stat fails with ENOTDIR, which os.IsNotExist does not cover; - a path segment longer than NAME_MAX fails with ENAMETOOLONG. Treat ENOTDIR and ENAMETOOLONG like a missing file and render the 404 page. Any other stat error (for example permission denied) is now passed to http.FileServer, which maps it to the proper status code instead of dereferencing a nil FileInfo. Add a table test for serveFilesHandler covering existing and missing files, a path below a regular file, an overlong segment and a directory without index.html. Fixes #857 Signed-off-by: NopeCharon <115794254+NopeCharon@users.noreply.github.com> --- backend/handlers.go | 9 ++++++++- backend/main_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/backend/handlers.go b/backend/handlers.go index fc0c07bd2..1f8e1966d 100644 --- a/backend/handlers.go +++ b/backend/handlers.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "html/template" "io" @@ -10,6 +11,7 @@ import ( "path" "path/filepath" "strings" + "syscall" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" @@ -99,10 +101,15 @@ func serveFilesHandler(fs http.FileSystem) http.Handler { upath = path.Clean(upath) fileInfo, err := os.Stat(fmt.Sprintf("%v%s", fs, upath)) if err != nil { - if os.IsNotExist(err) { + // A path below a regular file (ENOTDIR, e.g. /index.html/foo) or with an + // overlong component (ENAMETOOLONG) cannot exist, so treat it as not found. + if os.IsNotExist(err) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENAMETOOLONG) { notFoundHandler(w, r) return } + // Let http.FileServer report any other error (e.g. 403 on permission denied). + fsh.ServeHTTP(w, r) + return } if fileInfo.IsDir() { diff --git a/backend/main_test.go b/backend/main_test.go index c03115264..2cc178c81 100644 --- a/backend/main_test.go +++ b/backend/main_test.go @@ -3,6 +3,9 @@ package main import ( "net/http" "net/http/httptest" + "os" + "path/filepath" + "strings" "testing" ) @@ -112,3 +115,35 @@ func TestStaticFileServer(t *testing.T) { t.Errorf("Wrong content type, expected %s, got %s", expectedContentType, contentType) } } + +func TestServeFilesHandler(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, "page.html"), []byte("page"), 0o644); err != nil { + t.Fatal(err) + } + + h := serveFilesHandler(http.Dir(root)) + + cases := []struct { + name string + path string + want int + }{ + {name: "existing file", path: "/page.html", want: http.StatusOK}, + {name: "missing file", path: "/missing.html", want: http.StatusNotFound}, + {name: "path below a regular file", path: "/page.html/foo", want: http.StatusNotFound}, + {name: "overlong path component", path: "/" + strings.Repeat("a", 300), want: http.StatusNotFound}, + {name: "directory without index", path: "/", want: http.StatusNotFound}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, tc.path, nil)) + + if rec.Code != tc.want { + t.Fatalf("unexpected status for %s: got %d want %d", tc.path, rec.Code, tc.want) + } + }) + } +}