From ea4dc757a3abd5f31179a4322163e6ffa48a9cf8 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 13:44:25 +0800 Subject: [PATCH] fix(middleware): keep handler 404 responses with Static HTML5 mode HTML5 fallback to index.html should apply only to router-level 404s, not to 404 errors returned by matched API handlers behind the static middleware. Fixes #2775 Co-authored-by: Cursor --- middleware/static.go | 14 +++++++++++++- middleware/static_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/middleware/static.go b/middleware/static.go index 2d946c178..0211a2ae5 100644 --- a/middleware/static.go +++ b/middleware/static.go @@ -204,7 +204,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { } var he *echo.HTTPError - if !(errors.As(err, &he) && config.HTML5 && he.Code == http.StatusNotFound) { + if !(errors.As(err, &he) && config.HTML5 && he.Code == http.StatusNotFound && isRouterNotFoundError(c, err)) { return err } @@ -246,6 +246,18 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc { } } +func isRouterNotFoundError(c echo.Context, err error) bool { + if errors.Is(err, echo.ErrNotFound) { + return true + } + switch c.Path() { + case "", "/*": + return true + default: + return false + } +} + func serveFile(c echo.Context, file http.File, info os.FileInfo) error { http.ServeContent(c.Response(), c.Request(), info.Name(), info.ModTime(), file) return nil diff --git a/middleware/static_test.go b/middleware/static_test.go index a9722c096..33acd6c46 100644 --- a/middleware/static_test.go +++ b/middleware/static_test.go @@ -454,3 +454,39 @@ func TestStatic_CustomFS(t *testing.T) { }) } } + +func TestStaticHTML5DoesNotOverrideHandler404(t *testing.T) { + t.Parallel() + + e := echo.New() + e.Use(StaticWithConfig(StaticConfig{ + Root: "../_fixture", + HTML5: true, + })) + + e.GET("/api/test/:id", func(c echo.Context) error { + if c.Param("id") == "3" { + return echo.NewHTTPError(http.StatusNotFound, "not found") + } + return c.String(http.StatusOK, "ID: "+c.Param("id")) + }) + + t.Run("handler 404 is preserved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/test/3", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Contains(t, rec.Body.String(), "not found") + assert.NotContains(t, rec.Body.String(), "Echo") + }) + + t.Run("router 404 serves SPA index", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/client-route", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "Echo") + }) +}