diff --git a/middleware/static.go b/middleware/static.go index 62cf04e34..200f96a30 100644 --- a/middleware/static.go +++ b/middleware/static.go @@ -269,10 +269,12 @@ func (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) { } var he echo.HTTPStatusCoder - if !errors.As(err, &he) || !config.HTML5 || he.StatusCode() != http.StatusNotFound { + if (c.Path() != "" && c.RouteInfo().Method != echo.RouteNotFound) || + !errors.As(err, &he) || + !config.HTML5 || he.StatusCode() != http.StatusNotFound { return err } - // is case HTML5 mode is enabled + echo 404 we serve index to the client + // In HTML5 mode, serve index for a router-level 404. file, err = currentFS.Open(config.Index) if err != nil { return err diff --git a/middleware/static_test.go b/middleware/static_test.go index 3567c0a55..889169eea 100644 --- a/middleware/static_test.go +++ b/middleware/static_test.go @@ -44,6 +44,67 @@ func TestStatic_useCaseForApiAndSPAs(t *testing.T) { } +func TestStaticHTML5PreservesMatchedHandlerNotFound(t *testing.T) { + var testCases = []struct { + name string + buildEcho func() *echo.Echo + whenURL string + expectCode int + expectJSONEq string + expectContains string + }{ + { + name: "ok, router-matched handler 404 is preserved instead of serving index", + buildEcho: func() *echo.Echo { + e := echo.New() + e.Use(StaticWithConfig(StaticConfig{ + Root: "testdata/dist/public", + HTML5: true, + })) + e.GET("/api/users/:id", func(c *echo.Context) error { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + }) + return e + }, + whenURL: "/api/users/42", + expectCode: http.StatusNotFound, + expectJSONEq: `{"message":"user not found"}`, + }, + { + name: "ok, router-level 404 for group without matched route still serves index", + buildEcho: func() *echo.Echo { + e := echo.New() + e.Group("/app", StaticWithConfig(StaticConfig{ + Root: "testdata/dist/public", + HTML5: true, + })) + return e + }, + whenURL: "/app/dashboard", + expectCode: http.StatusOK, + expectContains: "

Hello from index

\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := tc.buildEcho() + + req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + assert.Equal(t, tc.expectCode, rec.Code) + if tc.expectJSONEq != "" { + assert.JSONEq(t, tc.expectJSONEq, rec.Body.String()) + } + if tc.expectContains != "" { + assert.Contains(t, rec.Body.String(), tc.expectContains) + } + }) + } +} + func TestStatic(t *testing.T) { var testCases = []struct { name string