mirror of
https://github.com/labstack/echo.git
synced 2025-01-07 23:01:56 +02:00
Changes from master (from 70acd57105
to 70acd57105
)
This commit is contained in:
parent
2b4c5a4097
commit
aad765a542
12
echo.go
12
echo.go
@ -153,6 +153,8 @@ const (
|
|||||||
PROPFIND = "PROPFIND"
|
PROPFIND = "PROPFIND"
|
||||||
// REPORT Method can be used to get information about a resource, see rfc 3253
|
// REPORT Method can be used to get information about a resource, see rfc 3253
|
||||||
REPORT = "REPORT"
|
REPORT = "REPORT"
|
||||||
|
// RouteNotFound is special method type for routes handling "route not found" (404) cases
|
||||||
|
RouteNotFound = "echo_route_not_found"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
@ -408,6 +410,16 @@ func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo
|
|||||||
return e.Add(http.MethodTrace, path, h, m...)
|
return e.Add(http.MethodTrace, path, h, m...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RouteNotFound registers a special-case route which is executed when no other route is found (i.e. HTTP 404 cases)
|
||||||
|
// for current request URL.
|
||||||
|
// Path supports static and named/any parameters just like other http method is defined. Generally path is ended with
|
||||||
|
// wildcard/match-any character (`/*`, `/download/*` etc).
|
||||||
|
//
|
||||||
|
// Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })`
|
||||||
|
func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {
|
||||||
|
return e.Add(RouteNotFound, path, h, m...)
|
||||||
|
}
|
||||||
|
|
||||||
// Any registers a new route for all supported HTTP methods and path with matching handler
|
// Any registers a new route for all supported HTTP methods and path with matching handler
|
||||||
// in the router with optional route-level middleware. Panics on error.
|
// in the router with optional route-level middleware. Panics on error.
|
||||||
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) Routes {
|
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) Routes {
|
||||||
|
64
echo_test.go
64
echo_test.go
@ -918,6 +918,70 @@ func TestEchoGroup(t *testing.T) {
|
|||||||
assert.Equal(t, "023", buf.String())
|
assert.Equal(t, "023", buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEcho_RouteNotFound(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
whenURL string
|
||||||
|
expectRoute interface{}
|
||||||
|
expectCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "404, route to static not found handler /a/c/xx",
|
||||||
|
whenURL: "/a/c/xx",
|
||||||
|
expectRoute: "GET /a/c/xx",
|
||||||
|
expectCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "404, route to path param not found handler /a/:file",
|
||||||
|
whenURL: "/a/echo.exe",
|
||||||
|
expectRoute: "GET /a/:file",
|
||||||
|
expectCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "404, route to any not found handler /*",
|
||||||
|
whenURL: "/b/echo.exe",
|
||||||
|
expectRoute: "GET /*",
|
||||||
|
expectCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "200, route /a/c/df to /a/c/df",
|
||||||
|
whenURL: "/a/c/df",
|
||||||
|
expectRoute: "GET /a/c/df",
|
||||||
|
expectCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
|
||||||
|
okHandler := func(c Context) error {
|
||||||
|
return c.String(http.StatusOK, c.Request().Method+" "+c.Path())
|
||||||
|
}
|
||||||
|
notFoundHandler := func(c Context) error {
|
||||||
|
return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
e.GET("/", okHandler)
|
||||||
|
e.GET("/a/c/df", okHandler)
|
||||||
|
e.GET("/a/b*", okHandler)
|
||||||
|
e.PUT("/*", okHandler)
|
||||||
|
|
||||||
|
e.RouteNotFound("/a/c/xx", notFoundHandler) // static
|
||||||
|
e.RouteNotFound("/a/:file", notFoundHandler) // param
|
||||||
|
e.RouteNotFound("/*", notFoundHandler) // any
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectCode, rec.Code)
|
||||||
|
assert.Equal(t, tc.expectRoute, rec.Body.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEchoNotFound(t *testing.T) {
|
func TestEchoNotFound(t *testing.T) {
|
||||||
e := New()
|
e := New()
|
||||||
req := httptest.NewRequest(http.MethodGet, "/files", nil)
|
req := httptest.NewRequest(http.MethodGet, "/files", nil)
|
||||||
|
7
group.go
7
group.go
@ -157,6 +157,13 @@ func (g *Group) File(path, file string, middleware ...MiddlewareFunc) RouteInfo
|
|||||||
return g.Add(http.MethodGet, path, handler, middleware...)
|
return g.Add(http.MethodGet, path, handler, middleware...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RouteNotFound implements `Echo#RouteNotFound()` for sub-routes within the Group.
|
||||||
|
//
|
||||||
|
// Example: `g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })`
|
||||||
|
func (g *Group) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {
|
||||||
|
return g.Add(RouteNotFound, path, h, m...)
|
||||||
|
}
|
||||||
|
|
||||||
// Add implements `Echo#Add()` for sub-routes within the Group. Panics on error.
|
// Add implements `Echo#Add()` for sub-routes within the Group. Panics on error.
|
||||||
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo {
|
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo {
|
||||||
ri, err := g.AddRoute(Route{
|
ri, err := g.AddRoute(Route{
|
||||||
|
@ -303,6 +303,71 @@ func TestGroup_TRACE(t *testing.T) {
|
|||||||
assert.Equal(t, `OK`, body)
|
assert.Equal(t, `OK`, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroup_RouteNotFound(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
whenURL string
|
||||||
|
expectRoute interface{}
|
||||||
|
expectCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "404, route to static not found handler /group/a/c/xx",
|
||||||
|
whenURL: "/group/a/c/xx",
|
||||||
|
expectRoute: "GET /group/a/c/xx",
|
||||||
|
expectCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "404, route to path param not found handler /group/a/:file",
|
||||||
|
whenURL: "/group/a/echo.exe",
|
||||||
|
expectRoute: "GET /group/a/:file",
|
||||||
|
expectCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "404, route to any not found handler /group/*",
|
||||||
|
whenURL: "/group/b/echo.exe",
|
||||||
|
expectRoute: "GET /group/*",
|
||||||
|
expectCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "200, route /group/a/c/df to /group/a/c/df",
|
||||||
|
whenURL: "/group/a/c/df",
|
||||||
|
expectRoute: "GET /group/a/c/df",
|
||||||
|
expectCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
g := e.Group("/group")
|
||||||
|
|
||||||
|
okHandler := func(c Context) error {
|
||||||
|
return c.String(http.StatusOK, c.Request().Method+" "+c.Path())
|
||||||
|
}
|
||||||
|
notFoundHandler := func(c Context) error {
|
||||||
|
return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
g.GET("/", okHandler)
|
||||||
|
g.GET("/a/c/df", okHandler)
|
||||||
|
g.GET("/a/b*", okHandler)
|
||||||
|
g.PUT("/*", okHandler)
|
||||||
|
|
||||||
|
g.RouteNotFound("/a/c/xx", notFoundHandler) // static
|
||||||
|
g.RouteNotFound("/a/:file", notFoundHandler) // param
|
||||||
|
g.RouteNotFound("/*", notFoundHandler) // any
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectCode, rec.Code)
|
||||||
|
assert.Equal(t, tc.expectRoute, rec.Body.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGroup_Any(t *testing.T) {
|
func TestGroup_Any(t *testing.T) {
|
||||||
e := New()
|
e := New()
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -72,9 +72,11 @@ func (config BasicAuthConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalid base64 shouldn't be treated as error
|
||||||
|
// instead should be treated as invalid client input
|
||||||
b, errDecode := base64.StdEncoding.DecodeString(auth[l+1:])
|
b, errDecode := base64.StdEncoding.DecodeString(auth[l+1:])
|
||||||
if errDecode != nil {
|
if errDecode != nil {
|
||||||
lastError = echo.ErrUnauthorized.WithInternal(fmt.Errorf("invalid basic auth value: %w", errDecode))
|
lastError = echo.NewHTTPError(http.StatusBadRequest).WithInternal(errDecode)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
idx := bytes.IndexByte(b, ':')
|
idx := bytes.IndexByte(b, ':')
|
||||||
|
@ -56,7 +56,7 @@ func TestBasicAuth(t *testing.T) {
|
|||||||
name: "nok, not base64 Authorization header",
|
name: "nok, not base64 Authorization header",
|
||||||
givenConfig: defaultConfig,
|
givenConfig: defaultConfig,
|
||||||
whenAuth: []string{strings.ToUpper(basic) + " NOT_BASE64"},
|
whenAuth: []string{strings.ToUpper(basic) + " NOT_BASE64"},
|
||||||
expectErr: "code=401, message=Unauthorized, internal=invalid basic auth value: illegal base64 data at input byte 3",
|
expectErr: "code=400, message=Bad Request, internal=illegal base64 data at input byte 3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "nok, missing Authorization header",
|
name: "nok, missing Authorization header",
|
||||||
|
@ -22,6 +22,8 @@ type LoggerConfig struct {
|
|||||||
// Tags to construct the logger format.
|
// Tags to construct the logger format.
|
||||||
//
|
//
|
||||||
// - time_unix
|
// - time_unix
|
||||||
|
// - time_unix_milli
|
||||||
|
// - time_unix_micro
|
||||||
// - time_unix_nano
|
// - time_unix_nano
|
||||||
// - time_rfc3339
|
// - time_rfc3339
|
||||||
// - time_rfc3339_nano
|
// - time_rfc3339_nano
|
||||||
@ -119,6 +121,10 @@ func (config LoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
|
|||||||
switch tag {
|
switch tag {
|
||||||
case "time_unix":
|
case "time_unix":
|
||||||
return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
|
return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
|
||||||
|
case "time_unix_milli":
|
||||||
|
return buf.WriteString(strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||||
|
case "time_unix_micro":
|
||||||
|
return buf.WriteString(strconv.FormatInt(time.Now().UnixMicro(), 10))
|
||||||
case "time_unix_nano":
|
case "time_unix_nano":
|
||||||
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10))
|
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||||
case "time_rfc3339":
|
case "time_rfc3339":
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -172,6 +173,52 @@ func TestLoggerCustomTimestamp(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoggerTemplateWithTimeUnixMilli(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
e.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Format: `${time_unix_milli}`,
|
||||||
|
Output: buf,
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.GET("/", func(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
unixMillis, err := strconv.ParseInt(buf.String(), 10, 64)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.WithinDuration(t, time.Unix(unixMillis/1000, 0), time.Now(), 3*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerTemplateWithTimeUnixMicro(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
e.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Format: `${time_unix_micro}`,
|
||||||
|
Output: buf,
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.GET("/", func(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
unixMicros, err := strconv.ParseInt(buf.String(), 10, 64)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.WithinDuration(t, time.Unix(unixMicros/1000000, 0), time.Now(), 3*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkLoggerWithConfig_withoutMapFields(b *testing.B) {
|
func BenchmarkLoggerWithConfig_withoutMapFields(b *testing.B) {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
|
99
router.go
99
router.go
@ -214,18 +214,22 @@ type routeMethod struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type routeMethods struct {
|
type routeMethods struct {
|
||||||
connect *routeMethod
|
connect *routeMethod
|
||||||
delete *routeMethod
|
delete *routeMethod
|
||||||
get *routeMethod
|
get *routeMethod
|
||||||
head *routeMethod
|
head *routeMethod
|
||||||
options *routeMethod
|
options *routeMethod
|
||||||
patch *routeMethod
|
patch *routeMethod
|
||||||
post *routeMethod
|
post *routeMethod
|
||||||
propfind *routeMethod
|
propfind *routeMethod
|
||||||
put *routeMethod
|
put *routeMethod
|
||||||
trace *routeMethod
|
trace *routeMethod
|
||||||
report *routeMethod
|
report *routeMethod
|
||||||
anyOther map[string]*routeMethod
|
anyOther map[string]*routeMethod
|
||||||
|
|
||||||
|
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
|
||||||
|
notFoundHandler *routeMethod
|
||||||
|
|
||||||
allowHeader string
|
allowHeader string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +257,9 @@ func (m *routeMethods) set(method string, r *routeMethod) {
|
|||||||
m.trace = r
|
m.trace = r
|
||||||
case REPORT:
|
case REPORT:
|
||||||
m.report = r
|
m.report = r
|
||||||
|
case RouteNotFound:
|
||||||
|
m.notFoundHandler = r
|
||||||
|
return // RouteNotFound/404 is not considered as a handler so no further logic needs to be executed
|
||||||
default:
|
default:
|
||||||
if m.anyOther == nil {
|
if m.anyOther == nil {
|
||||||
m.anyOther = make(map[string]*routeMethod)
|
m.anyOther = make(map[string]*routeMethod)
|
||||||
@ -357,6 +364,7 @@ func (m *routeMethods) isHandler() bool {
|
|||||||
m.trace != nil ||
|
m.trace != nil ||
|
||||||
m.report != nil ||
|
m.report != nil ||
|
||||||
len(m.anyOther) != 0
|
len(m.anyOther) != 0
|
||||||
|
// RouteNotFound/404 is not considered as a handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routes returns all registered routes
|
// Routes returns all registered routes
|
||||||
@ -615,7 +623,12 @@ func (r *DefaultRouter) insert(t kind, path string, method string, ri routeMetho
|
|||||||
}
|
}
|
||||||
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
|
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
|
||||||
} else if lcpLen < prefixLen {
|
} else if lcpLen < prefixLen {
|
||||||
// Split node
|
// Split node into two before we insert new node.
|
||||||
|
// This happens when we are inserting path that is submatch of any existing inserted paths.
|
||||||
|
// For example, we have node `/test` and now are about to insert `/te/*`. In that case
|
||||||
|
// 1. overlapping part is `/te` that is used as parent node
|
||||||
|
// 2. `st` is part from existing node that is not matching - it gets its own node (child to `/te`)
|
||||||
|
// 3. `/*` is the new part we are about to insert (child to `/te`)
|
||||||
n := newNode(
|
n := newNode(
|
||||||
currentNode.kind,
|
currentNode.kind,
|
||||||
currentNode.prefix[lcpLen:],
|
currentNode.prefix[lcpLen:],
|
||||||
@ -739,10 +752,8 @@ func (n *node) findStaticChild(l byte) *node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *node) findChildWithLabel(l byte) *node {
|
func (n *node) findChildWithLabel(l byte) *node {
|
||||||
for _, c := range n.staticChildren {
|
if c := n.findStaticChild(l); c != nil {
|
||||||
if c.label == l {
|
return c
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if l == paramLabel {
|
if l == paramLabel {
|
||||||
return n.paramChild
|
return n.paramChild
|
||||||
@ -755,11 +766,7 @@ func (n *node) findChildWithLabel(l byte) *node {
|
|||||||
|
|
||||||
func (n *node) setHandler(method string, r *routeMethod) {
|
func (n *node) setHandler(method string, r *routeMethod) {
|
||||||
n.methods.set(method, r)
|
n.methods.set(method, r)
|
||||||
if r != nil && r.handler != nil {
|
n.isHandler = n.methods.isHandler()
|
||||||
n.isHandler = true
|
|
||||||
} else {
|
|
||||||
n.isHandler = n.methods.isHandler()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: notFoundRouteInfo exists to avoid allocations when setting 404 RouteInfo to Context
|
// Note: notFoundRouteInfo exists to avoid allocations when setting 404 RouteInfo to Context
|
||||||
@ -904,7 +911,7 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc {
|
|||||||
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
|
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
|
||||||
nk, ok := backtrackToNextNodeKind(staticKind)
|
nk, ok := backtrackToNextNodeKind(staticKind)
|
||||||
if !ok {
|
if !ok {
|
||||||
break // No other possibilities on the decision path
|
break // No other possibilities on the decision path, handler will be whatever context is reset to.
|
||||||
} else if nk == paramKind {
|
} else if nk == paramKind {
|
||||||
goto Param
|
goto Param
|
||||||
// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
|
// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
|
||||||
@ -920,15 +927,21 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc {
|
|||||||
search = search[lcpLen:]
|
search = search[lcpLen:]
|
||||||
searchIndex = searchIndex + lcpLen
|
searchIndex = searchIndex + lcpLen
|
||||||
|
|
||||||
// Finish routing if no remaining search and we are on a node with handler and matching method type
|
// Finish routing if is no request path remaining to search
|
||||||
if search == "" && currentNode.isHandler {
|
if search == "" {
|
||||||
// check if current node has handler registered for http method we are looking for. we store currentNode as
|
// in case of node that is handler we have exact method type match or something for 405 to use
|
||||||
// best matching in case we do no find no more routes matching this path+method
|
if currentNode.isHandler {
|
||||||
if previousBestMatchNode == nil {
|
// check if current node has handler registered for http method we are looking for. we store currentNode as
|
||||||
previousBestMatchNode = currentNode
|
// best matching in case we do no find no more routes matching this path+method
|
||||||
}
|
if previousBestMatchNode == nil {
|
||||||
if rMethod := currentNode.methods.find(req.Method); rMethod != nil {
|
previousBestMatchNode = currentNode
|
||||||
matchedRouteMethod = rMethod
|
}
|
||||||
|
if h := currentNode.methods.find(req.Method); h != nil {
|
||||||
|
matchedRouteMethod = h
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if currentNode.methods.notFoundHandler != nil {
|
||||||
|
matchedRouteMethod = currentNode.methods.notFoundHandler
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -948,7 +961,8 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc {
|
|||||||
i := 0
|
i := 0
|
||||||
l := len(search)
|
l := len(search)
|
||||||
if currentNode.isLeaf {
|
if currentNode.isLeaf {
|
||||||
// when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
|
// when param node does not have any children (path param is last piece of route path) then param node should
|
||||||
|
// act similarly to any node - consider all remaining search as match
|
||||||
i = l
|
i = l
|
||||||
} else {
|
} else {
|
||||||
for ; i < l && search[i] != '/'; i++ {
|
for ; i < l && search[i] != '/'; i++ {
|
||||||
@ -973,13 +987,16 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc {
|
|||||||
searchIndex += +len(search)
|
searchIndex += +len(search)
|
||||||
search = ""
|
search = ""
|
||||||
|
|
||||||
// check if current node has handler registered for http method we are looking for. we store currentNode as
|
if rMethod := currentNode.methods.find(req.Method); rMethod != nil {
|
||||||
// best matching in case we do no find no more routes matching this path+method
|
matchedRouteMethod = rMethod
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// we store currentNode as best matching in case we do not find more routes matching this path+method. Needed for 405
|
||||||
if previousBestMatchNode == nil {
|
if previousBestMatchNode == nil {
|
||||||
previousBestMatchNode = currentNode
|
previousBestMatchNode = currentNode
|
||||||
}
|
}
|
||||||
if rMethod := currentNode.methods.find(req.Method); rMethod != nil {
|
if currentNode.methods.notFoundHandler != nil {
|
||||||
matchedRouteMethod = rMethod
|
matchedRouteMethod = currentNode.methods.notFoundHandler
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1021,7 +1038,13 @@ func (r *DefaultRouter) Route(c RoutableContext) HandlerFunc {
|
|||||||
|
|
||||||
rPath = currentNode.originalPath
|
rPath = currentNode.originalPath
|
||||||
rInfo = notFoundRouteInfo
|
rInfo = notFoundRouteInfo
|
||||||
if currentNode.isHandler {
|
if currentNode.methods.notFoundHandler != nil {
|
||||||
|
matchedRouteMethod = currentNode.methods.notFoundHandler
|
||||||
|
|
||||||
|
rInfo = matchedRouteMethod.routeInfo
|
||||||
|
rPath = matchedRouteMethod.path
|
||||||
|
rHandler = matchedRouteMethod.handler
|
||||||
|
} else if currentNode.isHandler {
|
||||||
rInfo = methodNotAllowedRouteInfo
|
rInfo = methodNotAllowedRouteInfo
|
||||||
|
|
||||||
c.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
|
c.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
|
||||||
|
226
router_test.go
226
router_test.go
@ -3190,6 +3190,232 @@ func TestDefaultRouter_OptionsMethodHandler(t *testing.T) {
|
|||||||
assert.Equal(t, "not empty", body)
|
assert.Equal(t, "not empty", body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouter_RouteWhenNotFoundRouteWithNodeSplitting(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
hf := func(c Context) error {
|
||||||
|
return c.String(http.StatusOK, c.RouteInfo().Name())
|
||||||
|
}
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/test*", Handler: hf, Name: "0"})
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/test*", Handler: hf, Name: "1"})
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/test", Handler: hf, Name: "2"})
|
||||||
|
|
||||||
|
// Tree before:
|
||||||
|
// 1 `/`
|
||||||
|
// 1.1 `*` (any) ID=1
|
||||||
|
// 1.2 `test` (static) ID=2
|
||||||
|
// 1.2.1 `*` (any) ID=0
|
||||||
|
|
||||||
|
// node with path `test` has routeNotFound handler from previous Add call. Now when we insert `/te/st*` into router tree
|
||||||
|
// This means that node `test` is split into `te` and `st` nodes and new node `/st*` is inserted.
|
||||||
|
// On that split `/test` routeNotFound handler must not be lost.
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/te/st*", Handler: hf, Name: "3"})
|
||||||
|
// Tree after:
|
||||||
|
// 1 `/`
|
||||||
|
// 1.1 `*` (any) ID=1
|
||||||
|
// 1.2 `te` (static)
|
||||||
|
// 1.2.1 `st` (static) ID=2
|
||||||
|
// 1.2.1.1 `*` (any) ID=0
|
||||||
|
// 1.2.2 `/st` (static)
|
||||||
|
// 1.2.2.1 `*` (any) ID=3
|
||||||
|
|
||||||
|
_, body := request(http.MethodPut, "/test", e)
|
||||||
|
|
||||||
|
assert.Equal(t, "2", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouter_RouteWhenNotFoundRouteAnyKind(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
whenURL string
|
||||||
|
expectRoute interface{}
|
||||||
|
expectID int
|
||||||
|
expectParam map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "route not existent /xx to not found handler /*",
|
||||||
|
whenURL: "/xx",
|
||||||
|
expectRoute: "/*",
|
||||||
|
expectID: 4,
|
||||||
|
expectParam: map[string]string{"*": "xx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route not existent /a/xx to not found handler /a/*",
|
||||||
|
whenURL: "/a/xx",
|
||||||
|
expectRoute: "/a/*",
|
||||||
|
expectID: 5,
|
||||||
|
expectParam: map[string]string{"*": "xx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route not existent /a/c/dxxx to not found handler /a/c/d*",
|
||||||
|
whenURL: "/a/c/dxxx",
|
||||||
|
expectRoute: "/a/c/d*",
|
||||||
|
expectID: 6,
|
||||||
|
expectParam: map[string]string{"*": "xxx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route /a/c/df to /a/c/df",
|
||||||
|
whenURL: "/a/c/df",
|
||||||
|
expectRoute: "/a/c/df",
|
||||||
|
expectID: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.contextPathParamAllocSize = 1
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/", Handler: handlerHelper("ID", 0), Name: "0"})
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/a/c/df", Handler: handlerHelper("ID", 1), Name: "1"})
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/a/b*", Handler: handlerHelper("ID", 2), Name: "2"})
|
||||||
|
r.Add(Route{Method: http.MethodPut, Path: "/*", Handler: handlerHelper("ID", 3), Name: "3"})
|
||||||
|
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/a/c/d*", Handler: handlerHelper("ID", 6), Name: "6"})
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/a/*", Handler: handlerHelper("ID", 5), Name: "5"})
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/*", Handler: handlerHelper("ID", 4), Name: "4"})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||||
|
c := e.NewContext(req, nil).(*DefaultContext)
|
||||||
|
|
||||||
|
handler := r.Route(c)
|
||||||
|
handler(c)
|
||||||
|
|
||||||
|
testValue, _ := c.Get("ID").(int)
|
||||||
|
assert.Equal(t, tc.expectID, testValue)
|
||||||
|
assert.Equal(t, tc.expectRoute, c.Path())
|
||||||
|
for param, expectedValue := range tc.expectParam {
|
||||||
|
assert.Equal(t, expectedValue, c.PathParam(param))
|
||||||
|
}
|
||||||
|
checkUnusedParamValues(t, c, tc.expectParam)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouter_RouteWhenNotFoundRouteParamKind(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
whenURL string
|
||||||
|
expectRoute interface{}
|
||||||
|
expectID int
|
||||||
|
expectParam map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "route not existent /xx to not found handler /:file",
|
||||||
|
whenURL: "/xx",
|
||||||
|
expectRoute: "/:file",
|
||||||
|
expectID: 4,
|
||||||
|
expectParam: map[string]string{"file": "xx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route not existent /a/xx to not found handler /a/:file",
|
||||||
|
whenURL: "/a/xx",
|
||||||
|
expectRoute: "/a/:file",
|
||||||
|
expectID: 5,
|
||||||
|
expectParam: map[string]string{"file": "xx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route not existent /a/c/dxxx to not found handler /a/c/d:file",
|
||||||
|
whenURL: "/a/c/dxxx",
|
||||||
|
expectRoute: "/a/c/d:file",
|
||||||
|
expectID: 6,
|
||||||
|
expectParam: map[string]string{"file": "xxx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route /a/c/df to /a/c/df",
|
||||||
|
whenURL: "/a/c/df",
|
||||||
|
expectRoute: "/a/c/df",
|
||||||
|
expectID: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.contextPathParamAllocSize = 1
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/", Handler: handlerHelper("ID", 0), Name: "0"})
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/a/c/df", Handler: handlerHelper("ID", 1), Name: "1"})
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/a/b*", Handler: handlerHelper("ID", 2), Name: "2"})
|
||||||
|
r.Add(Route{Method: http.MethodPut, Path: "/*", Handler: handlerHelper("ID", 3), Name: "3"})
|
||||||
|
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/a/c/d:file", Handler: handlerHelper("ID", 6), Name: "6"})
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/a/:file", Handler: handlerHelper("ID", 5), Name: "5"})
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/:file", Handler: handlerHelper("ID", 4), Name: "4"})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||||
|
c := e.NewContext(req, nil).(*DefaultContext)
|
||||||
|
|
||||||
|
handler := r.Route(c)
|
||||||
|
handler(c)
|
||||||
|
|
||||||
|
testValue, _ := c.Get("ID").(int)
|
||||||
|
assert.Equal(t, tc.expectID, testValue)
|
||||||
|
assert.Equal(t, tc.expectRoute, c.Path())
|
||||||
|
for param, expectedValue := range tc.expectParam {
|
||||||
|
assert.Equal(t, expectedValue, c.PathParam(param))
|
||||||
|
}
|
||||||
|
checkUnusedParamValues(t, c, tc.expectParam)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouter_RouteWhenNotFoundRouteStaticKind(t *testing.T) {
|
||||||
|
// note: static not found handler is quite silly thing to have but we still support it
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
whenURL string
|
||||||
|
expectRoute interface{}
|
||||||
|
expectID int
|
||||||
|
expectParam map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "route not existent / to not found handler /",
|
||||||
|
whenURL: "/",
|
||||||
|
expectRoute: "/",
|
||||||
|
expectID: 3,
|
||||||
|
expectParam: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route /a to /a",
|
||||||
|
whenURL: "/a",
|
||||||
|
expectRoute: "/a",
|
||||||
|
expectID: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.contextPathParamAllocSize = 1
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(Route{Method: http.MethodPut, Path: "/", Handler: handlerHelper("ID", 0), Name: "0"})
|
||||||
|
r.Add(Route{Method: http.MethodGet, Path: "/a", Handler: handlerHelper("ID", 1), Name: "1"})
|
||||||
|
r.Add(Route{Method: http.MethodPut, Path: "/*", Handler: handlerHelper("ID", 2), Name: "2"})
|
||||||
|
|
||||||
|
r.Add(Route{Method: RouteNotFound, Path: "/", Handler: handlerHelper("ID", 3), Name: "3"})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||||
|
c := e.NewContext(req, nil).(*DefaultContext)
|
||||||
|
|
||||||
|
handler := r.Route(c)
|
||||||
|
handler(c)
|
||||||
|
|
||||||
|
testValue, _ := c.Get("ID").(int)
|
||||||
|
assert.Equal(t, tc.expectID, testValue)
|
||||||
|
assert.Equal(t, tc.expectRoute, c.Path())
|
||||||
|
for param, expectedValue := range tc.expectParam {
|
||||||
|
assert.Equal(t, expectedValue, c.PathParam(param))
|
||||||
|
}
|
||||||
|
checkUnusedParamValues(t, c, tc.expectParam)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type mySimpleRouter struct {
|
type mySimpleRouter struct {
|
||||||
route Route
|
route Route
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user