labstack / echo

High performance, minimalist Go web framework
https://echo.labstack.com
MIT License
29.54k stars 2.21k forks source link

middleware + group => 404 is returned when 405 should be returned #1981

Closed croissanne closed 1 year ago

croissanne commented 3 years ago

Issue Description

When using a middleware in combination with a group, 404 is returned when 405 should be returned.

Checklist

Expected behaviour

Return 405 when calling the endpoint behind the group with a wrong method.

Actual behaviour

Received 404.

Steps to reproduce

minimal reproducer:

package main

import (
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    e := echo.New()
    g := e.Group("/api")
    g.Use(middleware.Logger())
    g.GET("/endpoint", func(c echo.Context) error {
        return c.String(200, "OK\n")
    })
    e.Start(":8080")
}

in a shell:

curl -X POST localhost:8080/api/endpoint

->{"message":"Not Found"}

Version/commit

This was broken in 643066594d00891e3151c7ed87244bfeddcd57b9, released in 4.3.0 and onward

croissanne commented 3 years ago

I took a stab at it but without a lot of knowledge of all the edge cases of the rather complex router code I have no idea if it's a good one.

tree: https://github.com/Gundersanne/echo/tree/1981 commit: 584291d81be55157100dc04754ef938c45565ec3 diff: git apply ...

diff --git a/router.go b/router.go
index 5b2474b..8fc19b1 100644
--- a/router.go
+++ b/router.go
@@ -526,10 +526,10 @@ func (r *Router) Find(method, path string, c Context) {
            // best matching in case we do no find no more routes matching this path+method
            if previousBestMatchNode == nil {
                previousBestMatchNode = currentNode
-           }
-           if h := currentNode.findHandler(method); h != nil {
-               matchedHandler = h
-               break
+               if h := currentNode.findHandler(method); h != nil {
+                   matchedHandler = h
+                   break
+               }
            }
        }

diff --git a/router_test.go b/router_test.go
index 71cedf8..3d1109f 100644
--- a/router_test.go
+++ b/router_test.go
@@ -747,11 +747,12 @@ func TestMethodNotAllowedAndNotFound(t *testing.T) {
            expectError: ErrMethodNotAllowed,
        },
        {
+           // isn't best match here a route which matches the path but not the method?
            name:        "best match is any route up in tree",
            whenMethod:  http.MethodGet,
            whenURL:     "/users/1",
-           expectRoute: "/*",
-           expectParam: map[string]string{"*": "users/1"},
+           expectRoute: nil,
+           expectError: ErrMethodNotAllowed,
        },
    }
    for _, tc := range testCases {
aldas commented 3 years ago

Probably is related to https://github.com/labstack/echo/blob/7f502b1ff10913aeab28abab64f2bf45952c768d/group.go#L28-L29

when group is created it adds 2 routes so middlewares will be executed for that group in case of 404

croissanne commented 3 years ago

Ah, that seems weird to me, returning the NotFoundHandler when there's a better candidate (path match but not method).

aldas commented 3 years ago

well it depends. for router match created by .Any is a valid route. Any adds a route for POST that results 404. Router does not know that result would be 404 and therefore does not look for 405.

I'll have to investigate this situation.

croissanne commented 3 years ago

Ah I get that, but I'm not sure if it should take priority over the exact path match as in the reproducer.

Note that with this patch calling the group point (localhost:8080/api) directly will still result in a 404. And in that case it's totally valid to return a 404 I agree, since there's no path which matches exactly. With an exact path match however (and in that case I think previousBestMatchNode will already be set in router.go:527, chances are a user is using the wrong method to call the endpoint they want.

aldas commented 3 years ago

If you comment out

    //g.Any("", NotFoundHandler)
    //g.Any("/*", NotFoundHandler)

you would get 405 but this means that your Logger middleware will not be fired for that request. When logger is added with e.Pre() it would be fired as pre mws are executed before routing is done.

just a note - group is pretty much helper for adding routes with same prefix and middlewares. it does not exist as a separate entity.

croissanne commented 3 years ago

Right, but with Use the Router definitely has to be involved no? Like the middleware should only apply with a request to a valid endpoint? So we can't just comment those lines out?

With Pre none of this applies of course.


edit: Not entirely sure I follow, but I think I see your point. But in the case of calling the group endpoint directly, previousBestMatchNode (on router.go:527) will be nil afaik, at which point it'll mark it as the best match and return 404?

aldas commented 3 years ago

well, removing those 2 lines would probably be surprise after update for some (probably rare cases?). We are planning to remove those lines in v5 but it is far in future and v5 has other "potentially breaking changes" also. Anyway this does not help us currently. If think those lines were added because of static middleware or something similar. I have investigated these lines git history before. As usual these things need a little bit thinking/investigation not to break too much.

Pre middlewares are executed even if there are no route match Group Use middlewares are executed only when there is a match. When you use Use with group this does not mean that these middlewares are immediately "registered" in Echo. Group Use middlewares exist/have meaning only when they are added to route handler chain - which Any in that case does.

For example:

    e := echo.New()
    g := e.Group("/api")
    g.Use(middleware.Logger())
    e.Start(":8080")

has 2 routes in runtime

croissanne commented 3 years ago

Yea and I think that behaviour remains intact for https://github.com/labstack/echo/commit/584291d81be55157100dc04754ef938c45565ec3. The commit message isn't entirely correct however, really it should be something like:

only return the notfoundhandler for anykind if no previous (exact) match/best candidate was found

Removing the two Any calls would be the better solution perhaps if the middlewares didn't have to be executed.

For instance:

    e := echo.New()
    g := e.Group("/api")
    g.Use(middleware.Logger())
    g2 := g.Group("/api2")
    g2.Use(middleware.Logger())
    g2.GET("/endpoint", func(c echo.Context) error {
        return c.String(200, "OK\n")
    })
    e.Start(":8080")

With a nested group you'd expect a call to /api return 404 and get logged once, and indeed:

{"time":"2021-09-08T23:33:04.652820416+02:00","id":"","remote_ip":"::1","host":"localhost:8080","method":"POST","uri":"/api/","user_agent":"curl/7.74.0","status":404,"error":"code=404, message=Not Found","latency":32823,"latency_human":"32.823µs","bytes_in":0,"bytes_out":24}

In this case /api gets matched, but does return 404 of course, as defined by those Any calls in Group.Use. Remove those 2 Any calls and you get 0 logging output because the router can't find any matches.

a call to /api/api2 has 2 logging calls and returns 404:

{"time":"2021-09-08T23:34:57.145792259+02:00","id":"","remote_ip":"::1","host":"localhost:8080","method":"POST","uri":"/api/api2","user_agent":"curl/7.74.0","status":404,"error":"code=404, message=Not Found","latency":111619,"latency_human":"111.619µs","bytes_in":0,"bytes_out":24}
{"time":"2021-09-08T23:34:57.146048684+02:00","id":"","remote_ip":"::1","host":"localhost:8080","method":"POST","uri":"/api/api2","user_agent":"curl/7.74.0","status":404,"error":"","latency":368607,"latency_human":"368.607µs","bytes_in":0,"bytes_out":24}

Which is what you'd expect right, it matches g, executes the logging middleware; it matches g2, executes the logging middleware again, and then returns 404. Again removing the two Any calls in results in no logging output, the router can't find any matches.

DylanDD13 commented 1 year ago

Was a workaround ever found for this issue ?

aldas commented 1 year ago

It is not but I think we can fix this by replacing

https://github.com/labstack/echo/blob/40eb889d14001640d8e9c48f22db8388dcf0feb5/group.go#L28-L30

with (probably the most correct in this situation as RouteNotFound is treated a little bit differently in routing)

    g.RouteNotFound("", NotFoundHandler)
    g.RouteNotFound("/*", NotFoundHandler)

and maybe add new method g.MethodNotAllowed(

I'll create PR for this

eroncastro commented 1 year ago

@aldas I think this issue can be closed, since #2411 was merged.