Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Routes and onhandlers #2337

Merged
merged 3 commits into from Dec 25, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
125 changes: 56 additions & 69 deletions echo.go
Expand Up @@ -3,41 +3,40 @@ Package echo implements high performance, minimalist Go web framework.

Example:

package main
package main

import (
"net/http"
import (
"net/http"

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

// Handler
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
// Handler
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}

func main() {
// Echo instance
e := echo.New()
func main() {
// Echo instance
e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Routes
e.GET("/", hello)
// Routes
e.GET("/", hello)

// Start server
e.Logger.Fatal(e.Start(":1323"))
}
// Start server
e.Logger.Fatal(e.Start(":1323"))
}

Learn more at https://echo.labstack.com
*/
package echo

import (
"bytes"
stdContext "context"
"crypto/tls"
"errors"
Expand All @@ -62,20 +61,28 @@ import (

type (
// Echo is the top-level framework instance.
//
// Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these
// fields from handlers/middlewares and changing field values at the same time leads to data-races.
// Same rule applies to adding new routes after server has been started - Adding a route is not Goroutine safe action.
aldas marked this conversation as resolved.
Show resolved Hide resolved
Echo struct {
filesystem
common
// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
// listener address info (on which interface/port was listener binded) without having data races.
startupMutex sync.RWMutex
startupMutex sync.RWMutex
colorer *color.Color

// premiddleware are middlewares that are run before routing is done. In case pre-middleware returns an error router
// will not be called at all and execution ends up in global error handler.
aldas marked this conversation as resolved.
Show resolved Hide resolved
premiddleware []MiddlewareFunc
middleware []MiddlewareFunc
maxParam *int
router *Router
routers map[string]*Router
pool sync.Pool

StdLogger *stdLog.Logger
colorer *color.Color
premiddleware []MiddlewareFunc
middleware []MiddlewareFunc
maxParam *int
router *Router
routers map[string]*Router
pool sync.Pool
Server *http.Server
TLSServer *http.Server
Listener net.Listener
Expand All @@ -93,6 +100,9 @@ type (
Logger Logger
IPExtractor IPExtractor
ListenerNetwork string

// OnAddRouteHandler is called when Echo adds new route to specific host router.
OnAddRouteHandler func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc)
lammel marked this conversation as resolved.
Show resolved Hide resolved
}

// Route contains a handler and information for matching against requests.
Expand Down Expand Up @@ -527,21 +537,20 @@ func (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route {
return e.file(path, file, e.GET, m...)
}

func (e *Echo) add(host, method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
name := handlerName(handler)
func (e *Echo) add(host, method, path string, handler HandlerFunc, middlewares ...MiddlewareFunc) *Route {
router := e.findRouter(host)
// FIXME: when handler+middleware are both nil ... make it behave like handler removal
router.Add(method, path, func(c Context) error {
h := applyMiddleware(handler, middleware...)
//FIXME: when handler+middleware are both nil ... make it behave like handler removal
name := handlerName(handler)
route := router.add(method, path, name, func(c Context) error {
h := applyMiddleware(handler, middlewares...)
return h(c)
})
r := &Route{
Method: method,
Path: path,
Name: name,

if e.OnAddRouteHandler != nil {
e.OnAddRouteHandler(host, *route, handler, middlewares)
}
e.router.routes[method+path] = r
return r

return route
}

// Add registers a new route for an HTTP method and path with matching handler
Expand Down Expand Up @@ -578,35 +587,13 @@ func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {

// Reverse generates an URL from route name and provided parameters.
func (e *Echo) Reverse(name string, params ...interface{}) string {
uri := new(bytes.Buffer)
ln := len(params)
n := 0
for _, r := range e.router.routes {
if r.Name == name {
for i, l := 0, len(r.Path); i < l; i++ {
if (r.Path[i] == ':' || r.Path[i] == '*') && n < ln {
for ; i < l && r.Path[i] != '/'; i++ {
}
uri.WriteString(fmt.Sprintf("%v", params[n]))
n++
}
if i < l {
uri.WriteByte(r.Path[i])
}
}
break
}
}
return uri.String()
return e.router.Reverse(name, params...)
}

// Routes returns the registered routes.
// Routes returns the registered routes for default router.
// In case when Echo serves multiple hosts/domains use `e.Routers()["domain2.site"].Routes()` to get specific host routes.
func (e *Echo) Routes() []*Route {
routes := make([]*Route, 0, len(e.router.routes))
for _, v := range e.router.routes {
routes = append(routes, v)
}
return routes
return e.router.Routes()
}

// AcquireContext returns an empty `Context` instance from the pool.
Expand Down Expand Up @@ -913,8 +900,8 @@ func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc {

// GetPath returns RawPath, if it's empty returns Path from URL
// Difference between RawPath and Path is:
// * Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.
// * RawPath is an optional field which only gets set if the default encoding is different from Path.
// - Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.
// - RawPath is an optional field which only gets set if the default encoding is different from Path.
func GetPath(r *http.Request) string {
path := r.URL.RawPath
if path == "" {
Expand Down
126 changes: 107 additions & 19 deletions echo_test.go
Expand Up @@ -531,34 +531,71 @@ func TestEchoRoutes(t *testing.T) {
}
}

func TestEchoRoutesHandleHostsProperly(t *testing.T) {
func TestEchoRoutesHandleAdditionalHosts(t *testing.T) {
e := New()
h := e.Host("route.com")
domain2Router := e.Host("domain2.router.com")
routes := []*Route{
{http.MethodGet, "/users/:user/events", ""},
{http.MethodGet, "/users/:user/events/public", ""},
{http.MethodPost, "/repos/:owner/:repo/git/refs", ""},
{http.MethodPost, "/repos/:owner/:repo/git/tags", ""},
}
for _, r := range routes {
h.Add(r.Method, r.Path, func(c Context) error {
domain2Router.Add(r.Method, r.Path, func(c Context) error {
return c.String(http.StatusOK, "OK")
})
}
e.Add(http.MethodGet, "/api", func(c Context) error {
return c.String(http.StatusOK, "OK")
})

if assert.Equal(t, len(routes), len(e.Routes())) {
for _, r := range e.Routes() {
found := false
for _, rr := range routes {
if r.Method == rr.Method && r.Path == rr.Path {
found = true
break
}
domain2Routes := e.Routers()["domain2.router.com"].Routes()

assert.Len(t, domain2Routes, len(routes))
for _, r := range domain2Routes {
found := false
for _, rr := range routes {
if r.Method == rr.Method && r.Path == rr.Path {
found = true
break
}
if !found {
t.Errorf("Route %s %s not found", r.Method, r.Path)
}
if !found {
t.Errorf("Route %s %s not found", r.Method, r.Path)
}
}
}

func TestEchoRoutesHandleDefaultHost(t *testing.T) {
e := New()
routes := []*Route{
{http.MethodGet, "/users/:user/events", ""},
{http.MethodGet, "/users/:user/events/public", ""},
{http.MethodPost, "/repos/:owner/:repo/git/refs", ""},
{http.MethodPost, "/repos/:owner/:repo/git/tags", ""},
}
for _, r := range routes {
e.Add(r.Method, r.Path, func(c Context) error {
return c.String(http.StatusOK, "OK")
})
}
e.Host("subdomain.mysite.site").Add(http.MethodGet, "/api", func(c Context) error {
return c.String(http.StatusOK, "OK")
})

defaultRouterRoutes := e.Routes()
assert.Len(t, defaultRouterRoutes, len(routes))
for _, r := range defaultRouterRoutes {
found := false
for _, rr := range routes {
if r.Method == rr.Method && r.Path == rr.Path {
found = true
break
}
}
if !found {
t.Errorf("Route %s %s not found", r.Method, r.Path)
}
}
}

Expand Down Expand Up @@ -1424,6 +1461,44 @@ func TestEchoListenerNetworkInvalid(t *testing.T) {
assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323"))
}

func TestEcho_OnAddRouteHandler(t *testing.T) {
type rr struct {
host string
route Route
handler HandlerFunc
middleware []MiddlewareFunc
}
dummyHandler := func(Context) error { return nil }
e := New()

added := make([]rr, 0)
e.OnAddRouteHandler = func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc) {
added = append(added, rr{
host: host,
route: route,
handler: handler,
middleware: middleware,
})
}

e.GET("/static", NotFoundHandler)
e.Host("domain.site").GET("/static/*", dummyHandler, func(next HandlerFunc) HandlerFunc {
return func(c Context) error {
return next(c)
}
})

assert.Len(t, added, 2)

assert.Equal(t, "", added[0].host)
assert.Equal(t, Route{Method: http.MethodGet, Path: "/static", Name: "github.com/labstack/echo/v4.glob..func1"}, added[0].route)
assert.Len(t, added[0].middleware, 0)

assert.Equal(t, "domain.site", added[1].host)
assert.Equal(t, Route{Method: http.MethodGet, Path: "/static/*", Name: "github.com/labstack/echo/v4.TestEcho_OnAddRouteHandler.func1"}, added[1].route)
assert.Len(t, added[1].middleware, 1)
}

func TestEchoReverse(t *testing.T) {
e := New()
dummyHandler := func(Context) error { return nil }
Expand Down Expand Up @@ -1451,14 +1526,27 @@ func TestEchoReverseHandleHostProperly(t *testing.T) {
dummyHandler := func(Context) error { return nil }

e := New()

// routes added to the default router are different form different hosts
e.GET("/static", dummyHandler).Name = "default-host /static"
e.GET("/static/*", dummyHandler).Name = "xxx"

// different host
h := e.Host("the_host")
h.GET("/static", dummyHandler).Name = "/static"
h.GET("/static/*", dummyHandler).Name = "/static/*"
h.GET("/static", dummyHandler).Name = "host2 /static"
h.GET("/static/v2/*", dummyHandler).Name = "xxx"

assert.Equal(t, "/static", e.Reverse("default-host /static"))
// when actual route does not have params and we provide some to Reverse we should get that route url back
assert.Equal(t, "/static", e.Reverse("default-host /static", "missing param"))

host2Router := e.Routers()["the_host"]
assert.Equal(t, "/static", host2Router.Reverse("host2 /static"))
assert.Equal(t, "/static", host2Router.Reverse("host2 /static", "missing param"))

assert.Equal(t, "/static/v2/*", host2Router.Reverse("xxx"))
assert.Equal(t, "/static/v2/foo.txt", host2Router.Reverse("xxx", "foo.txt"))

assert.Equal(t, "/static", e.Reverse("/static"))
assert.Equal(t, "/static", e.Reverse("/static", "missing param"))
assert.Equal(t, "/static/*", e.Reverse("/static/*"))
assert.Equal(t, "/static/foo.txt", e.Reverse("/static/*", "foo.txt"))
}

func TestEcho_ListenerAddr(t *testing.T) {
Expand Down