echo icon indicating copy to clipboard operation
echo copied to clipboard

RouteNotFound handler does not falls back to root one

Open endorama opened this issue 2 years ago • 7 comments

Issue Description

I think I found a bug in how RouteNotFound handler is propagated to route groups.

When using groups with a dedicated middleware there is no fallback to the root RouteNotFound handler, while that happens when no middleware is specified.

Checklist

  • [ ] Dependencies installed (not sure what this means)
  • [x] No typos
  • [x] Searched existing issues and docs

Expected behaviour

The root RouteNotFound handler is used.

Actual behaviour

An internal(?) handler is used

Steps to reproduce

Put the code below in a test file, run go test ./...

Working code to debug

This is a test file that reproduce the issue.

package main_test

import (
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/require"
)

func server() http.Handler {
	e := echo.New()
	e.HideBanner = true
	e.HidePort = true

	// expect this handler is used as fallback unless a more specific is present
	e.RouteNotFound("/*", missingRouteHandler)

	// addig a middleware to the group overrides the RouteNotFound handler
	v0 := e.Group("/v0", func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			return next(c)
		}
	})
	// adding this would make it work
	// v0.RouteNotFound("/*", missingRouteHandler)
	v0.POST("/resource", handler)

	// the same does not happen when no middleware is set
	v1 := e.Group("/v1")
	v1.POST("/resource", handler)

	return e
}

func missingRouteHandler(c echo.Context) error {
	return c.NoContent(http.StatusNotFound)
}

func handler(c echo.Context) error {
	return nil
}

func TestMissing(t *testing.T) {
	srv := httptest.NewServer(server())

	routes := []string{
		// this is successful
		"/foo",
		// this fails
        //   Error Trace:    /home/endorama/code/REDACTED/main_test.go:68
        //   Error:          "{"message":"Not Found"}
        //                   " should have 0 item(s), but has 24
        //   Test:           TestMissing//v0/foo
		"/v0/foo",
		// this is successful
		"/v1/foo",
	}

	for _, path := range routes {
		t.Run(path, func(t *testing.T) {
			req, _ := http.NewRequest("POST", srv.URL+path, nil)

			resp, err := srv.Client().Do(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			b, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			require.Equal(t, http.StatusNotFound, resp.StatusCode)
			require.Len(t, b, 0)
			require.Equal(t, "0", resp.Header.Get("Content-Length"))
		})
	}
}

Version/commit

github.com/labstack/echo/v4 v4.11.1

endorama avatar Jul 18 '23 13:07 endorama

this is somewhat of a "feature" because of this block

https://github.com/labstack/echo/blob/a2e7085094bda23a674c887f0e93f4a15245c439/group.go#L21-L32

aldas avatar Jul 18 '23 13:07 aldas

@aldas thanks for your reply!

From a framework user perspective this behaviour is surprising. The fact that it happens only when a middleware is added to the group make it feel more like an unintended side effect than a desired behaviour.

The reason why this behaviour happens is also a bit obscure, in particular the wording are only executed if they are added to the Router with route. Are middlewares for groups executed only when a matching route is found? Does that include global and group middlewares or only group middlewares?

Would it be possible to consider a change in behaviour to make it less surprising? From the framework user perspective the ideal behaviour would be to use Echo global route not found handlers for paths "" and /* instead of the default ones when they are defined. Could this be achieved with the current framework design?

In case this isn't be possible, would it be ok to add/update the documentation to signal this particular behaviour?

endorama avatar Jul 18 '23 15:07 endorama

I think I have found a bug, that is either related to this issue or happens because of the same "somewhat feature".

Due to the added NotFoundHandlers when using Middlewares on Groups, echo does not return a method not allowed status when using the wrong method. It always returns a not found status, even if the route exists with a different method.

package main_test

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/require"
)

func server() http.Handler {
	e := echo.New()

	// group without middle ware
	g1 := e.Group("/api/v1")
	g1.GET("/hello", func(c echo.Context) error {
		return c.String(200, "Hello, World!")
	})

	// group with middle ware
	g2 := e.Group("/api/v2")
	g2.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			return next(c)
		}
	})
	g2.GET("/hello", func(c echo.Context) error {
		return c.String(200, "Hello, World!")
	})

	return e
}

func TestMethodNotAllowed(t *testing.T) {
	srv := httptest.NewServer(server())

	req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/v1/hello", nil)
	resp, err := srv.Client().Do(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode, "/api/v1/hello should not allow POST and return 405")

	req, _ = http.NewRequest(http.MethodPost, srv.URL+"/api/v2/hello", nil)
	resp, err = srv.Client().Do(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode, "/api/v2/hello should not allow POST and return 405")
}

escb005 avatar Jan 03 '24 08:01 escb005

@escb005, yep, it does not seems to work properly. I'll take a look into it. It is probably time to investigate how to remove these 2 hidden routes that are added and replace them with something else that helps to execute middlewares.

aldas avatar Jan 03 '24 08:01 aldas