Add fallback handlers

Former-commit-id: f7e9bd17076a10e1ed1702780d7ce9e89f00b592
This commit is contained in:
Frédéric Meyer 2018-02-21 12:27:01 +03:00
parent 66209cae4f
commit 72b096e156
10 changed files with 322 additions and 16 deletions

View File

@ -5,6 +5,7 @@ package main
import (
"github.com/kataras/iris"
"github.com/kataras/iris/core/router"
"github.com/iris-contrib/middleware/cors"
)
@ -14,6 +15,7 @@ func main() {
app := iris.New()
crs := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, // allows everything, use that to change the hosts.
AllowedMethods: router.AllMethods[:],
AllowCredentials: true,
})
@ -29,6 +31,12 @@ func main() {
v1.Post("/send", func(ctx iris.Context) {
ctx.WriteString("sent")
})
v1.Put("/send", func(ctx iris.Context) {
ctx.WriteString("updated")
})
v1.Delete("/send", func(ctx iris.Context) {
ctx.WriteString("deleted")
})
}
// or use that to wrap the entire router

View File

@ -0,0 +1,29 @@
package main
import (
"github.com/kataras/iris"
)
func main() {
app := iris.New()
// this works as expected now,
// will handle *all* expect DELETE requests, even if there is no routes
app.Get("/action/{p}", h)
app.Run(iris.Addr(":8080"), ctx.Method(), ctx.Path(), iris.WithoutServerError(iris.ErrServerClosed))
}
func h(ctx iris.Context) {
ctx.Writef("[%s] %s : Parameter = `%s`", ctx.Params().Get("p"))
}
func fallbackHandler(ctx iris.Context) {
if ctx.Method() == "DELETE" {
ctx.Next()
return
}
ctx.Writef("[%s] %s : From fallback handler", ctx.Method(), ctx.Path())
}

View File

@ -48,4 +48,7 @@ type Application interface {
// If a handler is not already registered,
// then it creates & registers a new trivial handler on the-fly.
FireErrorCode(ctx Context)
// RouteExists checks if a route exists
RouteExists(method string, path string, ctx Context) bool
}

View File

@ -905,6 +905,9 @@ type Context interface {
// It's for extreme use cases, 99% of the times will never be useful for you.
Exec(method string, path string)
// RouteExists checks if a route exists
RouteExists(method string, path string) bool
// Application returns the iris app instance which belongs to this context.
// Worth to notice that this function returns an interface
// of the Application, which contains methods that are safe
@ -3130,6 +3133,11 @@ func (ctx *context) Exec(method string, path string) {
}
}
// RouteExists checks if a route exists
func (ctx *context) RouteExists(method string, path string) bool {
return ctx.Application().RouteExists(method, path, ctx)
}
// Application returns the iris app instance which belongs to this context.
// Worth to notice that this function returns an interface
// of the Application, which contains methods that are safe

View File

@ -273,7 +273,7 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
// per-party/children
middleware: middleware,
doneHandlers: api.doneHandlers,
fallbackStack: api.fallbackStack,
fallbackStack: api.fallbackStack.Fork(),
relativePath: fullpath,
}
}
@ -426,13 +426,19 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) {
api.doneGlobalHandlers = append(api.doneGlobalHandlers, handlers...)
}
// Fallback appends Handler(s) to the current Party's fallback stack.
// Fallback appends Handler(s) to the current fallback stack.
// Handler(s) is(are) called from Fallback stack when no route found and before sending NotFound status.
// Therefore Handler(s) in Fallback stack could send another thing than NotFound status,
// if `Context.Next()` method is not called.
// Done & DoneGlobal Handlers are not called.
func (api *APIBuilder) Fallback(middleware ...context.Handler) {
api.fallbackStack.add(middleware)
api.fallbackStack.Add(middleware)
}
// FallBackStack returns Fallback stack, this is implementation of interface RoutesProvider
// that is used in Router building by the RequestHandler.
func (api *APIBuilder) GetFallBackStack() *FallbackStack {
return api.fallbackStack
}
// Reset removes all the begin and done handlers that may derived from the parent party via `Use` & `Done`,

View File

@ -2,37 +2,71 @@ package router
import (
"net/http"
"sync"
"github.com/kataras/iris/context"
)
// FallbackStack is a stack (with LIFO calling order) for fallback handlers
// A fallback handler(s) is(are) called from Fallback stack
// when no route found and before sending NotFound status.
// Therefore Handler(s) in Fallback stack could send another thing than NotFound status,
// if `Context.Next()` method is not called.
// Done & DoneGlobal Handlers are not called.
type FallbackStack struct {
parent *FallbackStack
handlers context.Handlers
m sync.Mutex
}
func (stk *FallbackStack) add(h context.Handlers) {
stk.m.Lock()
defer stk.m.Unlock()
// _size is a terminal recursive method for computing size the stack
func (stk *FallbackStack) _size(i int) int {
res := i + len(stk.handlers)
if stk.parent == nil {
return res
}
return stk.parent._size(res)
}
// populate is a recursive method for concatenating handlers to `list` parameter
func (stk *FallbackStack) populate(list context.Handlers) {
n := copy(list, stk.handlers)
if stk.parent != nil {
stk.parent.populate(list[n:])
}
}
// Size gives the size of the full stack hierarchy
func (stk *FallbackStack) Size() int {
return stk._size(0)
}
// Add appends handlers to the beginning of the stack to have a LIFO calling order
func (stk *FallbackStack) Add(h context.Handlers) {
stk.handlers = append(stk.handlers, h...)
copy(stk.handlers[len(h):], stk.handlers)
copy(stk.handlers, h)
}
func (stk *FallbackStack) list() context.Handlers {
res := make(context.Handlers, len(stk.handlers))
// Fork make a new stack from this stack, and so create a stack child (leaf from a tree of stacks)
func (stk *FallbackStack) Fork() *FallbackStack {
return &FallbackStack{
parent: stk,
}
}
stk.m.Lock()
defer stk.m.Unlock()
copy(res, stk.handlers)
// List concatenate all handlers in stack hierarchy
func (stk *FallbackStack) List() context.Handlers {
res := make(context.Handlers, stk.Size())
stk.populate(res)
return res
}
// NewFallbackStack create a new Fallback stack with as first entry
// a handler which send NotFound status (the default)
func NewFallbackStack() *FallbackStack {
return &FallbackStack{
handlers: context.Handlers{

View File

@ -0,0 +1,112 @@
package router_test
import (
"testing"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/router"
"github.com/kataras/iris/httptest"
)
func TestFallbackStackAdd(t *testing.T) {
l := make([]string, 0)
stk := &router.FallbackStack{}
stk.Add(context.Handlers{
func(context.Context) {
l = append(l, "POS1")
},
})
stk.Add(context.Handlers{
func(context.Context) {
l = append(l, "POS2")
},
})
if stk.Size() != 2 {
t.Fatalf("Bad size (%d != 2)", stk.Size())
}
for _, h := range stk.List() {
h(nil)
}
if (l[0] != "POS2") || (l[1] != "POS1") {
t.Fatal("Bad positions: ", l)
}
}
func TestFallbackStackFork(t *testing.T) {
l := make([]string, 0)
stk := &router.FallbackStack{}
stk.Add(context.Handlers{
func(context.Context) {
l = append(l, "POS1")
},
})
stk.Add(context.Handlers{
func(context.Context) {
l = append(l, "POS2")
},
})
stk = stk.Fork()
stk.Add(context.Handlers{
func(context.Context) {
l = append(l, "POS3")
},
})
stk.Add(context.Handlers{
func(context.Context) {
l = append(l, "POS4")
},
})
if stk.Size() != 4 {
t.Fatalf("Bad size (%d != 4)", stk.Size())
}
for _, h := range stk.List() {
h(nil)
}
if (l[0] != "POS4") || (l[1] != "POS3") || (l[2] != "POS2") || (l[3] != "POS1") {
t.Fatal("Bad positions: ", l)
}
}
func TestFallbackStackCall(t *testing.T) {
// build the api
app := iris.New()
// setup an existing routes
app.Handle("GET", "/route", func(ctx context.Context) {
ctx.WriteString("ROUTED")
})
// setup fallback handler
app.Fallback(func(ctx context.Context) {
if ctx.Method() != "GET" {
ctx.Next()
return
}
ctx.WriteString("FALLBACK")
})
// run the tests
e := httptest.New(t, app, httptest.Debug(false))
e.Request("GET", "/route").Expect().Status(iris.StatusOK).Body().Equal("ROUTED")
e.Request("POST", "/route").Expect().Status(iris.StatusNotFound)
e.Request("POST", "/noroute").Expect().Status(iris.StatusNotFound)
e.Request("GET", "/noroute").Expect().Status(iris.StatusOK).Body().Equal("FALLBACK")
}

View File

@ -22,6 +22,8 @@ type RequestHandler interface {
HandleRequest(context.Context)
// Build should builds the handler, it's being called on router's BuildRouter.
Build(provider RoutesProvider) error
// RouteExists checks if a route exists
RouteExists(method, path string, ctx context.Context) bool
}
type tree struct {
@ -84,12 +86,13 @@ type RoutesProvider interface { // api builder
GetRoutes() []*Route
GetRoute(routeName string) *Route
FallBackStack() *FallbackStack
GetFallBackStack() *FallbackStack
}
func (h *routerHandler) Build(provider RoutesProvider) error {
registeredRoutes := provider.GetRoutes()
h.trees = h.trees[0:0] // reset, inneed when rebuilding.
h.fallbackStack = provider.GetFallBackStack()
// sort, subdomains goes first.
sort.Slice(registeredRoutes, func(i, j int) bool {
@ -245,5 +248,62 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
}
}
ctx.Do(h.fallbackStack.list())
if h.fallbackStack == nil {
ctx.StatusCode(http.StatusNotFound)
} else {
ctx.Do(h.fallbackStack.List())
}
}
// RouteExists checks if a route exists
func (h *routerHandler) RouteExists(method, path string, ctx context.Context) bool {
for i := range h.trees {
t := h.trees[i]
if method != t.Method {
continue
}
if h.hosts && t.Subdomain != "" {
requestHost := ctx.Host()
if netutil.IsLoopbackSubdomain(requestHost) {
// this fixes a bug when listening on
// 127.0.0.1:8080 for example
// and have a wildcard subdomain and a route registered to root domain.
continue // it's not a subdomain, it's something like 127.0.0.1 probably
}
// it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty
if t.Subdomain == SubdomainWildcardIndicator {
// mydomain.com -> invalid
// localhost -> invalid
// sub.mydomain.com -> valid
// sub.localhost -> valid
serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
if serverHost == requestHost {
continue // it's not a subdomain, it's a full domain (with .com...)
}
dotIdx := strings.IndexByte(requestHost, '.')
slashIdx := strings.IndexByte(requestHost, '/')
if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) {
// if "." was found anywhere but not at the first path segment (host).
} else {
continue
}
// continue to that, any subdomain is valid.
} else if !strings.HasPrefix(requestHost, t.Subdomain) { // t.Subdomain contains the dot.
continue
}
}
_, handlers := t.Nodes.Find(path, ctx.Params())
if len(handlers) > 0 {
// found
return true
}
// not found or method not allowed.
break
}
return false
}

View File

@ -147,6 +147,11 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
router.mainHandler(w, r)
}
// RouteExists checks if a route exists
func (router *Router) RouteExists(method, path string, ctx context.Context) bool {
return router.requestHandler.RouteExists(method, path, ctx)
}
type wrapper struct {
router http.HandlerFunc // http.HandlerFunc to catch the CURRENT state of its .ServeHTTP on case of future change.
wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc)

View File

@ -0,0 +1,41 @@
package router_test
import (
"testing"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/httptest"
)
func TestRouteExists(t *testing.T) {
// build the api
app := iris.New()
emptyHandler := func(context.Context) {}
// setup the tested routes
app.Handle("GET", "/route-exists", emptyHandler)
app.Handle("POST", "/route-with-param/{param}", emptyHandler)
// check RouteExists
app.Handle("GET", "/route-test", func(ctx context.Context) {
if ctx.RouteExists("GET", "/route-not-exists") {
t.Error("Route with path should not exists")
}
if ctx.RouteExists("POST", "/route-exists") {
t.Error("Route with method should not exists")
}
if !ctx.RouteExists("GET", "/route-exists") {
t.Error("Route 1 should exists")
}
if !ctx.RouteExists("POST", "/route-with-param/a-param") {
t.Error("Route 2 should exists")
}
})
// run the tests
httptest.New(t, app, httptest.Debug(false)).Request("GET", "/route-test").Expect().Status(iris.StatusOK)
}