HTTP error handlers per Party (docs and details in progress)

Former-commit-id: 7092ebed556b56d9f1769b9b23f2340c2a3a18f7
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-05-11 00:44:54 +03:00
parent 3657aaf240
commit c039730521
18 changed files with 434 additions and 306 deletions

View File

@ -8,6 +8,8 @@ Descubra lo que [otros dicen sobre Iris](https://iris-go.com/testimonials/) y **
[![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/)
[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks)
## Aprende Iris ## Aprende Iris
<details> <details>

View File

@ -12,6 +12,8 @@
[![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/)
[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks)
## آموزش آیریس ## آموزش آیریس
<details> <details>

View File

@ -8,6 +8,8 @@
[![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/)
[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks)
## Μαθαίνοντας το Iris ## Μαθαίνοντας το Iris
<details> <details>

View File

@ -8,6 +8,8 @@ Iris는 단순하고 빠르며 좋은 성능과 모든 기능을 갖춘 Go언어
[![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/)
[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks)
## Iris 배우기 ## Iris 배우기
<details> <details>

View File

@ -7,6 +7,8 @@ Iris — это быстрый, простой, но полнофункцион
[![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/)
[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks)
## Изучение Iris ## Изучение Iris
<details> <details>

View File

@ -8,6 +8,8 @@ Iris 是基于 Go 编写的一个快速,简单但功能齐全且非常高效
[![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/) [![](https://media.giphy.com/media/j5WLmtvwn98VPrm7li/giphy.gif)](https://iris-go.com/testimonials/)
[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks)
## 学习 Iris ## 学习 Iris
<details> <details>

View File

@ -13,7 +13,8 @@ import (
- Build should builds the handler, it's being called on router's BuildRouter. - Build should builds the handler, it's being called on router's BuildRouter.
Build(provider router.RoutesProvider) error Build(provider router.RoutesProvider) error
- RouteExists reports whether a particular route exists. - RouteExists reports whether a particular route exists.
RouteExists(ctx iris.Context, method, path string) bool RouteExists(ctx iris.Context, method, path string) bool
- FireErrorCode(ctx context.Context) should handle the given ctx.GetStatusCode().
For a more detailed, complete and useful example For a more detailed, complete and useful example
you can take a look at the iris' router itself which is located at: you can take a look at the iris' router itself which is located at:
@ -44,7 +45,7 @@ func (r *customRouter) HandleRequest(ctx iris.Context) {
} }
} }
ctx.SetCurrentRouteName(route.Name) ctx.SetCurrentRoute(route.ReadOnly)
ctx.Do(route.Handlers) ctx.Do(route.Handlers)
return return
} }
@ -71,6 +72,11 @@ func (r *customRouter) RouteExists(ctx iris.Context, method, path string) bool {
return false return false
} }
func (r *customRouter) FireErrorCode(ctx iris.Context) {
// responseStatusCode := ctx.GetStatusCode() // set by prior ctx.StatusCode calls
// [...]
}
func main() { func main() {
app := iris.New() app := iris.New()

View File

@ -61,9 +61,6 @@ type Application interface {
// FireErrorCode executes an error http status code handler // FireErrorCode executes an error http status code handler
// based on the context's status code. // based on the context's status code.
//
// If a handler is not already registered,
// then it creates & registers a new trivial handler on the-fly.
FireErrorCode(ctx Context) FireErrorCode(ctx Context)
// RouteExists reports whether a particular route exists // RouteExists reports whether a particular route exists

View File

@ -138,18 +138,13 @@ type Context interface {
// ctx.ResetRequest(r.WithContext(stdCtx)). // ctx.ResetRequest(r.WithContext(stdCtx)).
ResetRequest(r *http.Request) ResetRequest(r *http.Request)
// SetCurrentRouteName sets the route's name internally, // SetCurrentRoutes sets the route internally,
// in order to be able to find the correct current "read-only" Route when // See `GetCurrentRoute()` method too.
// end-developer calls the `GetCurrentRoute()` function. // It's being initialized by the Router.
// It's being initialized by the Router, if you change that name // See `Exec` or `SetHandlers/AddHandler` methods to simulate a request.
// manually nothing really happens except that you'll get other SetCurrentRoute(route RouteReadOnly)
// route via `GetCurrentRoute()`. // GetCurrentRoute returns the current "read-only" route that
// Instead, to execute a different path // was registered to this request's path.
// from this context you should use the `Exec` function
// or change the handlers via `SetHandlers/AddHandler` functions.
SetCurrentRouteName(currentRouteName string)
// GetCurrentRoute returns the current registered "read-only" route that
// was being registered to this request's path.
GetCurrentRoute() RouteReadOnly GetCurrentRoute() RouteReadOnly
// Do calls the SetHandlers(handlers) // Do calls the SetHandlers(handlers)
@ -175,7 +170,7 @@ type Context interface {
// HandlerIndex sets the current index of the // HandlerIndex sets the current index of the
// current context's handlers chain. // current context's handlers chain.
// If -1 passed then it just returns the // If n < 0 or the current handlers length is 0 then it just returns the
// current handler index without change the current index. // current handler index without change the current index.
// //
// Look Handlers(), Next() and StopExecution() too. // Look Handlers(), Next() and StopExecution() too.
@ -1194,9 +1189,9 @@ type context struct {
writer ResponseWriter writer ResponseWriter
// the original http.Request // the original http.Request
request *http.Request request *http.Request
// the current route's name registered to this request path. // the current route registered to this request path.
currentRouteName string currentRoute RouteReadOnly
deferFunc Handler deferFunc Handler
// the local key-value storage // the local key-value storage
params RequestParams // url named parameters. params RequestParams // url named parameters.
@ -1230,6 +1225,7 @@ func NewContext(app Application) Context {
// 4. response writer to the http.ResponseWriter. // 4. response writer to the http.ResponseWriter.
// 5. request to the *http.Request. // 5. request to the *http.Request.
func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) {
ctx.currentRoute = nil
ctx.handlers = nil // will be filled by router.Serve/HTTP ctx.handlers = nil // will be filled by router.Serve/HTTP
ctx.values = ctx.values[0:0] // >> >> by context.Values().Set ctx.values = ctx.values[0:0] // >> >> by context.Values().Set
ctx.params.Store = ctx.params.Store[0:0] ctx.params.Store = ctx.params.Store[0:0]
@ -1266,8 +1262,8 @@ func (ctx *context) EndRequest() {
ctx.deferFunc(ctx) ctx.deferFunc(ctx)
} }
if StatusCodeNotSuccessful(ctx.GetStatusCode()) && if !ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() &&
!ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() { StatusCodeNotSuccessful(ctx.GetStatusCode()) {
// author's note: // author's note:
// if recording, the error handler can handle // if recording, the error handler can handle
// the rollback and remove any response written before, // the rollback and remove any response written before,
@ -1332,23 +1328,18 @@ func (ctx *context) ResetRequest(r *http.Request) {
ctx.request = r ctx.request = r
} }
// SetCurrentRouteName sets the route's name internally, // SetCurrentRoutes sets the route internally,
// in order to be able to find the correct current "read-only" Route when // See `GetCurrentRoute()` method too.
// end-developer calls the `GetCurrentRoute()` function. // It's being initialized by the Router.
// It's being initialized by the Router, if you change that name // See `Exec` or `SetHandlers/AddHandler` methods to simulate a request.
// manually nothing really happens except that you'll get other func (ctx *context) SetCurrentRoute(route RouteReadOnly) {
// route via `GetCurrentRoute()`. ctx.currentRoute = route
// Instead, to execute a different path
// from this context you should use the `Exec` function
// or change the handlers via `SetHandlers/AddHandler` functions.
func (ctx *context) SetCurrentRouteName(currentRouteName string) {
ctx.currentRouteName = currentRouteName
} }
// GetCurrentRoute returns the current registered "read-only" route that // GetCurrentRoute returns the current "read-only" route that
// was being registered to this request's path. // was registered to this request's path.
func (ctx *context) GetCurrentRoute() RouteReadOnly { func (ctx *context) GetCurrentRoute() RouteReadOnly {
return ctx.app.GetRouteReadOnly(ctx.currentRouteName) return ctx.currentRoute
} }
// Do calls the SetHandlers(handlers) // Do calls the SetHandlers(handlers)
@ -1385,8 +1376,8 @@ func (ctx *context) Handlers() Handlers {
// HandlerIndex sets the current index of the // HandlerIndex sets the current index of the
// current context's handlers chain. // current context's handlers chain.
// If -1 passed then it just returns the // If n < 0 or the current handlers length is 0 then it just returns the
// current handler index without change the current index.rns that index, useless return value. // current handler index without change the current index.
// //
// Look Handlers(), Next() and StopExecution() too. // Look Handlers(), Next() and StopExecution() too.
func (ctx *context) HandlerIndex(n int) (currentIndex int) { func (ctx *context) HandlerIndex(n int) (currentIndex int) {
@ -1455,9 +1446,13 @@ func (ctx *context) HandlerFileLine() (file string, line int) {
} }
// RouteName returns the route name that this handler is running on. // RouteName returns the route name that this handler is running on.
// Note that it will return empty on not found handlers. // Note that it may return empty on not found handlers.
func (ctx *context) RouteName() string { func (ctx *context) RouteName() string {
return ctx.currentRouteName if ctx.currentRoute == nil {
return ""
}
return ctx.currentRoute.Name()
} }
// Next is the function that executed when `ctx.Next()` is called. // Next is the function that executed when `ctx.Next()` is called.

View File

@ -17,6 +17,10 @@ type RouteReadOnly interface {
// Name returns the route's name. // Name returns the route's name.
Name() string Name() string
// StatusErrorCode returns 0 for common resource routes
// or the error code that an http error handler registered on.
StatusErrorCode() int
// Method returns the route's method. // Method returns the route's method.
Method() string Method() string

View File

@ -58,7 +58,7 @@ func (repo *repository) getRelative(r *Route) *Route {
} }
for _, route := range repo.routes { for _, route := range repo.routes {
if r.Subdomain == route.Subdomain && r.Method == route.Method && r.FormattedPath == route.FormattedPath && !route.tmpl.IsTrailing() { if r.Subdomain == route.Subdomain && r.StatusCode == route.StatusCode && r.Method == route.Method && r.FormattedPath == route.FormattedPath && !route.tmpl.IsTrailing() {
return route return route
} }
} }
@ -100,12 +100,16 @@ func (repo *repository) register(route *Route, rule RouteRegisterRule) (*Route,
} }
} }
// fmt.Printf("repo.routes append:\t%#+v\n\n", route)
repo.routes = append(repo.routes, route) repo.routes = append(repo.routes, route)
if repo.pos == nil {
repo.pos = make(map[string]int) if route.StatusCode == 0 { // a common resource route, not a status code error handler.
if repo.pos == nil {
repo.pos = make(map[string]int)
}
repo.pos[route.tmpl.Src] = len(repo.routes) - 1
} }
repo.pos[route.tmpl.Src] = len(repo.routes) - 1
return route, nil return route, nil
} }
@ -117,8 +121,6 @@ type APIBuilder struct {
// the api builder global macros registry // the api builder global macros registry
macros *macro.Macros macros *macro.Macros
// the api builder global handlers per status code registry (used for custom http errors)
errorCodeHandlers *ErrorCodeHandlers
// the api builder global routes repository // the api builder global routes repository
routes *repository routes *repository
@ -164,12 +166,11 @@ var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handl
// which is responsible to build the API and the router handler. // which is responsible to build the API and the router handler.
func NewAPIBuilder() *APIBuilder { func NewAPIBuilder() *APIBuilder {
return &APIBuilder{ return &APIBuilder{
macros: macro.Defaults, macros: macro.Defaults,
errorCodeHandlers: defaultErrorCodeHandlers(), errors: errgroup.New("API Builder"),
errors: errgroup.New("API Builder"), relativePath: "/",
relativePath: "/", routes: new(repository),
routes: new(repository), apiBuilderDI: &APIContainer{Container: hero.New()},
apiBuilderDI: &APIContainer{Container: hero.New()},
} }
} }
@ -274,7 +275,11 @@ func (api *APIBuilder) SetRegisterRule(rule RouteRegisterRule) Party {
// //
// Returns a *Route, app will throw any errors later on. // Returns a *Route, app will throw any errors later on.
func (api *APIBuilder) Handle(method string, relativePath string, handlers ...context.Handler) *Route { func (api *APIBuilder) Handle(method string, relativePath string, handlers ...context.Handler) *Route {
routes := api.CreateRoutes([]string{method}, relativePath, handlers...) return api.handle(0, method, relativePath, handlers...)
}
func (api *APIBuilder) handle(errorCode int, method string, relativePath string, handlers ...context.Handler) *Route {
routes := api.createRoutes(errorCode, []string{method}, relativePath, handlers...)
var route *Route // the last one is returned. var route *Route // the last one is returned.
var err error var err error
@ -282,6 +287,7 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co
if route == nil { if route == nil {
break break
} }
// global // global
route.topLink = api.routes.getRelative(route) route.topLink = api.routes.getRelative(route)
@ -417,8 +423,18 @@ func (api *APIBuilder) HandleDir(requestPath, directory string, opts ...DirOptio
// This method can be used for third-parties Iris helpers packages and tools // This method can be used for third-parties Iris helpers packages and tools
// that want a more detailed view of Party-based Routes before take the decision to register them. // that want a more detailed view of Party-based Routes before take the decision to register them.
func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route { func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route {
if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any return api.createRoutes(0, methods, relativePath, handlers...)
return api.Any(relativePath, handlers...) }
func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePath string, handlers ...context.Handler) []*Route {
if statusCodeSuccessful(errorCode) {
errorCode = 0
}
if errorCode == 0 {
if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any
return api.Any(relativePath, handlers...)
}
} }
// no clean path yet because of subdomain indicator/separator which contains a dot. // no clean path yet because of subdomain indicator/separator which contains a dot.
@ -444,11 +460,17 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl
// So if we just put `api.middleware` or `api.doneHandlers` // So if we just put `api.middleware` or `api.doneHandlers`
// then the next `Party` will have those updated handlers // then the next `Party` will have those updated handlers
// but dev may change the rules for that child Party, so we have to make clones of them here. // but dev may change the rules for that child Party, so we have to make clones of them here.
var ( var (
beginHandlers = joinHandlers(api.middleware, context.Handlers{}) beginHandlers context.Handlers
doneHandlers = joinHandlers(api.doneHandlers, context.Handlers{}) doneHandlers context.Handlers
) )
if errorCode == 0 {
beginHandlers = joinHandlers(api.middleware, beginHandlers)
doneHandlers = joinHandlers(api.doneHandlers, doneHandlers)
}
mainHandlers := context.Handlers(handlers) mainHandlers := context.Handlers(handlers)
// before join the middleware + handlers + done handlers and apply the execution rules. // before join the middleware + handlers + done handlers and apply the execution rules.
@ -476,8 +498,8 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl
routes := make([]*Route, len(methods)) routes := make([]*Route, len(methods))
for i, m := range methods { for i, m := range methods { // single, empty method for error handlers.
route, err := NewRoute(m, subdomain, path, routeHandlers, *api.macros) route, err := NewRoute(errorCode, m, subdomain, path, routeHandlers, *api.macros)
if err != nil { // template path parser errors: if err != nil { // template path parser errors:
api.errors.Addf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path) api.errors.Addf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path)
continue continue
@ -523,6 +545,13 @@ func removeDuplicates(elements []string) (result []string) {
// //
// You can even declare a subdomain with relativePath as "mysub." or see `Subdomain`. // You can even declare a subdomain with relativePath as "mysub." or see `Subdomain`.
func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party { func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party {
// if app.Party("/"), root party, then just add the middlewares
// and return itself.
if api.relativePath == "/" && (relativePath == "" || relativePath == "/") {
api.Use(handlers...)
return api
}
parentPath := api.relativePath parentPath := api.relativePath
dot := string(SubdomainPrefix[0]) dot := string(SubdomainPrefix[0])
if len(parentPath) > 0 && parentPath[0] == '/' && strings.HasSuffix(relativePath, dot) { if len(parentPath) > 0 && parentPath[0] == '/' && strings.HasSuffix(relativePath, dot) {
@ -557,7 +586,6 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
// global/api builder // global/api builder
macros: api.macros, macros: api.macros,
routes: api.routes, routes: api.routes,
errorCodeHandlers: api.errorCodeHandlers,
beginGlobalHandlers: api.beginGlobalHandlers, beginGlobalHandlers: api.beginGlobalHandlers,
doneGlobalHandlers: api.doneGlobalHandlers, doneGlobalHandlers: api.doneGlobalHandlers,
errors: api.errors, errors: api.errors,
@ -674,7 +702,7 @@ func (api *APIBuilder) GetRoutesReadOnly() []context.RouteReadOnly {
routes := api.GetRoutes() routes := api.GetRoutes()
readOnlyRoutes := make([]context.RouteReadOnly, len(routes)) readOnlyRoutes := make([]context.RouteReadOnly, len(routes))
for i, r := range routes { for i, r := range routes {
readOnlyRoutes[i] = routeReadOnlyWrapper{r} readOnlyRoutes[i] = r.ReadOnly
} }
return readOnlyRoutes return readOnlyRoutes
@ -692,7 +720,7 @@ func (api *APIBuilder) GetRouteReadOnly(routeName string) context.RouteReadOnly
if r == nil { if r == nil {
return nil return nil
} }
return routeReadOnlyWrapper{r} return r.ReadOnly
} }
// GetRouteReadOnlyByPath returns the registered read-only route based on the template path (`Route.Tmpl().Src`). // GetRouteReadOnlyByPath returns the registered read-only route based on the template path (`Route.Tmpl().Src`).
@ -702,7 +730,7 @@ func (api *APIBuilder) GetRouteReadOnlyByPath(tmplPath string) context.RouteRead
return nil return nil
} }
return routeReadOnlyWrapper{r} return r.ReadOnly
} }
// Use appends Handler(s) to the current Party's routes and child routes. // Use appends Handler(s) to the current Party's routes and child routes.
@ -951,11 +979,25 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route {
// and/or disable the gzip if gzip response recorder // and/or disable the gzip if gzip response recorder
// was active. // was active.
func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) { func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) {
if len(api.beginGlobalHandlers) > 0 { // TODO: think a stable way for that and document it so end-developers
handlers = joinHandlers(api.beginGlobalHandlers, handlers) // not be suprised. Many feature requests in the past asked for that per-party error handlers.
if api.relativePath != "/" {
api.handle(statusCode, "", "/{tail:path}", handlers...)
} }
api.errorCodeHandlers.Register(statusCode, handlers...) api.handle(statusCode, "", "/", handlers...)
// if api.relativePath != "/" /* root is OK, no need to wildcard, see handler.go */ &&
// !strings.HasSuffix(api.relativePath, "}") /* and not /users/{id:int} */ {
// // We need to register the /users and the /users/{tail:path},
// api.handle(statusCode, "", "/{tail:path}", handlers...)
// }
// if strings.HasSuffix(api.relativePath, "/") {
// api.handle(statusCode, "", "/", handlers...)
// }
// api.handle(statusCode, "", "/{tail:path}", handlers...)
} }
// OnAnyErrorCode registers a handler which called when error status code written. // OnAnyErrorCode registers a handler which called when error status code written.
@ -972,15 +1014,6 @@ func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) {
} }
} }
// FireErrorCode executes an error http status code handler
// based on the context's status code.
//
// If a handler is not already registered,
// it creates and registers a new trivial handler on the-fly.
func (api *APIBuilder) FireErrorCode(ctx context.Context) {
api.errorCodeHandlers.Fire(ctx)
}
// Layout overrides the parent template layout with a more specific layout for this Party. // Layout overrides the parent template layout with a more specific layout for this Party.
// It returns the current Party. // It returns the current Party.
// //

View File

@ -15,26 +15,42 @@ import (
"github.com/kataras/pio" "github.com/kataras/pio"
) )
// RequestHandler the middle man between acquiring a context and releasing it. type (
// By-default is the router algorithm. // RequestHandler the middle man between acquiring a context and releasing it.
type RequestHandler interface { // By-default is the router algorithm.
// HandleRequest should handle the request based on the Context. RequestHandler interface {
HandleRequest(ctx context.Context) // Note: A different interface in order to hide the rest of the implementation.
// Build should builds the handler, it's being called on router's BuildRouter. // We only need the `FireErrorCode` to be accessible through the Iris application (see `iris.go#Build`)
Build(provider RoutesProvider) error HTTPErrorHandler
// RouteExists reports whether a particular route exists.
RouteExists(ctx context.Context, method, path string) bool // HandleRequest should handle the request based on the Context.
} HandleRequest(ctx context.Context)
// Build should builds the handler, it's being called on router's BuildRouter.
Build(provider RoutesProvider) error
// RouteExists reports whether a particular route exists.
RouteExists(ctx context.Context, method, path string) bool
}
// HTTPErrorHandler should contain a method `FireErrorCode` which
// handles http unsuccessful status codes.
HTTPErrorHandler interface {
FireErrorCode(ctx context.Context)
}
)
type routerHandler struct { type routerHandler struct {
config context.ConfigurationReadOnly config context.ConfigurationReadOnly
logger *golog.Logger logger *golog.Logger
trees []*trie trees []*trie
hosts bool // true if at least one route contains a Subdomain. errorTrees []*trie
hosts bool // true if at least one route contains a Subdomain.
errorHosts bool // true if error handlers are registered to at least one Subdomain.
} }
var _ RequestHandler = &routerHandler{} var _ RequestHandler = (*routerHandler)(nil)
var _ HTTPErrorHandler = (*routerHandler)(nil)
// NewDefaultHandler returns the handler which is responsible // NewDefaultHandler returns the handler which is responsible
// to map the request with a route (aka mux implementation). // to map the request with a route (aka mux implementation).
@ -45,7 +61,17 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge
} }
} }
func (h *routerHandler) getTree(method, subdomain string) *trie { func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie {
if statusCode > 0 {
for i := range h.errorTrees {
t := h.errorTrees[i]
if t.statusCode == statusCode && t.subdomain == subdomain {
return t
}
}
return nil
}
for i := range h.trees { for i := range h.trees {
t := h.trees[i] t := h.trees[i]
if t.method == method && t.subdomain == subdomain { if t.method == method && t.subdomain == subdomain {
@ -59,23 +85,28 @@ func (h *routerHandler) getTree(method, subdomain string) *trie {
// AddRoute registers a route. See `Router.AddRouteUnsafe`. // AddRoute registers a route. See `Router.AddRouteUnsafe`.
func (h *routerHandler) AddRoute(r *Route) error { func (h *routerHandler) AddRoute(r *Route) error {
var ( var (
routeName = r.Name method = r.Method
method = r.Method statusCode = r.StatusCode
subdomain = r.Subdomain subdomain = r.Subdomain
path = r.Path path = r.Path
handlers = r.Handlers handlers = r.Handlers
) )
t := h.getTree(method, subdomain) t := h.getTree(statusCode, method, subdomain)
if t == nil { if t == nil {
n := newTrieNode() n := newTrieNode()
// first time we register a route to this method with this subdomain // first time we register a route to this method with this subdomain
t = &trie{method: method, subdomain: subdomain, root: n} t = &trie{statusCode: statusCode, method: method, subdomain: subdomain, root: n}
h.trees = append(h.trees, t) if statusCode > 0 {
h.errorTrees = append(h.errorTrees, t)
} else {
h.trees = append(h.trees, t)
}
} }
t.insert(path, routeName, handlers) t.insert(path, r.ReadOnly, handlers)
return nil return nil
} }
@ -141,7 +172,11 @@ func (h *routerHandler) Build(provider RoutesProvider) error {
} }
if r.Subdomain != "" { if r.Subdomain != "" {
h.hosts = true if r.StatusCode > 0 {
h.errorHosts = true
} else {
h.hosts = true
}
} }
if r.topLink == nil { if r.topLink == nil {
@ -242,28 +277,73 @@ func bindMultiParamTypesHandler(top *Route, r *Route) {
return // should never happen, previous checks made to set the top link. return // should never happen, previous checks made to set the top link.
} }
currentStatusCode := r.StatusCode
if currentStatusCode == 0 {
currentStatusCode = http.StatusOK
}
decisionHandler := func(ctx context.Context) { decisionHandler := func(ctx context.Context) {
// println("core/router/handler.go: decision handler; " + ctx.Path() + " route.Name: " + r.Name + " vs context's " + ctx.GetCurrentRoute().Name()) // println("core/router/handler.go: decision handler; " + ctx.Path() + " route.Name: " + r.Name + " vs context's " + ctx.GetCurrentRoute().Name())
currentRouteName := ctx.RouteName() currentRoute := ctx.GetCurrentRoute()
// Different path parameters types in the same path, fallback should registered first e.g. {path} {string}, // Different path parameters types in the same path, fallback should registered first e.g. {path} {string},
// because the handler on this case is executing from last to top. // because the handler on this case is executing from last to top.
if f(ctx) { if f(ctx) {
// println("core/router/handler.go: filter for : " + r.Name + " passed") // println("core/router/handler.go: filter for : " + r.Name + " passed")
ctx.SetCurrentRouteName(r.Name) ctx.SetCurrentRoute(r.ReadOnly)
// Note: error handlers will be the same, routes came from the same party,
// no need to update them.
ctx.HandlerIndex(0) ctx.HandlerIndex(0)
ctx.Do(h) ctx.Do(h)
return return
} }
ctx.SetCurrentRouteName(currentRouteName) ctx.SetCurrentRoute(currentRoute)
ctx.StatusCode(http.StatusOK) ctx.StatusCode(currentStatusCode)
ctx.Next() ctx.Next()
} }
r.topLink.beginHandlers = append(context.Handlers{decisionHandler}, r.topLink.beginHandlers...) r.topLink.beginHandlers = append(context.Handlers{decisionHandler}, r.topLink.beginHandlers...)
} }
func (h *routerHandler) canHandleSubdomain(ctx context.Context, subdomain string) bool {
if subdomain == "" {
return true
}
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.
return false // 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 subdomain == SubdomainWildcardIndicator {
// mydomain.com -> invalid
// localhost -> invalid
// sub.mydomain.com -> valid
// sub.localhost -> valid
serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
if serverHost == requestHost {
return false // 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 {
return false
}
// continue to that, any subdomain is valid.
} else if !strings.HasPrefix(requestHost, subdomain) { // subdomain contains the dot.
return false
}
return true
}
func (h *routerHandler) HandleRequest(ctx context.Context) { func (h *routerHandler) HandleRequest(ctx context.Context) {
method := ctx.Method() method := ctx.Method()
path := ctx.Path() path := ctx.Path()
@ -304,40 +384,13 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
continue continue
} }
if h.hosts && t.subdomain != "" { if h.hosts && !h.canHandleSubdomain(ctx, t.subdomain) {
requestHost := ctx.Host() continue
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 := config.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
}
} }
n := t.search(path, ctx.Params()) n := t.search(path, ctx.Params())
if n != nil { if n != nil {
ctx.SetCurrentRouteName(n.RouteName) ctx.SetCurrentRoute(n.Route)
ctx.Do(n.Handlers) ctx.Do(n.Handlers)
// found // found
return return
@ -364,6 +417,89 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
ctx.StatusCode(http.StatusNotFound) ctx.StatusCode(http.StatusNotFound)
} }
func (h *routerHandler) FireErrorCode(ctx context.Context) {
statusCode := ctx.GetStatusCode() // the response's cached one.
// if we can reset the body
if w, ok := ctx.IsRecording(); ok {
if statusCodeSuccessful(w.StatusCode()) { // if not an error status code
w.WriteHeader(statusCode) // then set it manually here, otherwise it should be set via ctx.StatusCode(...)
}
// reset if previous content and it's recorder, keep the status code.
w.ClearHeaders()
w.ResetBody()
} else if w, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok {
// reset and disable the gzip in order to be an expected form of http error result
w.ResetBody()
w.Disable()
} else {
// if we can't reset the body and the body has been filled
// which means that the status code already sent,
// then do not fire this custom error code.
if ctx.ResponseWriter().Written() > 0 { // != -1, rel: context/context.go#EndRequest
return
}
}
for i := range h.errorTrees {
t := h.errorTrees[i]
if statusCode != t.statusCode {
continue
}
if h.errorHosts && !h.canHandleSubdomain(ctx, t.subdomain) {
continue
}
n := t.search(ctx.Path(), ctx.Params())
if n == nil {
// try to take the root's one.
n = t.root.getChild(pathSep)
}
if n != nil {
// fire this http status code's error handlers chain.
// ctx.StopExecution() // not uncomment this, is here to remember why to.
// note for me: I don't stopping the execution of the other handlers
// because may the user want to add a fallback error code
// i.e
// users := app.Party("/users")
// users.Done(func(ctx context.Context){ if ctx.StatusCode() == 400 { /* custom error code for /users */ }})
// use .HandlerIndex
// that sets the current handler index to zero
// in order to:
// ignore previous runs that may changed the handler index,
// via ctx.Next or ctx.StopExecution, if any.
//
// use .Do
// that overrides the existing handlers and sets and runs these error handlers.
// in order to:
// ignore the route's after-handlers, if any.
ctx.SetCurrentRoute(n.Route)
// Should work with:
// Manual call of ctx.Application().FireErrorCode(ctx) (handlers length > 0)
// And on `ctx.SetStatusCode`: Context -> EndRequest -> FireErrorCode (handlers length > 0)
// And on router: HandleRequest -> SetStatusCode -> Context ->
// EndRequest -> FireErrorCode (handlers' length is always 0)
ctx.HandlerIndex(0)
ctx.Do(n.Handlers)
return
}
break
}
// not error handler found, write a default message.
ctx.WriteString(http.StatusText(statusCode))
}
func statusCodeSuccessful(statusCode int) bool {
return !context.StatusCodeNotSuccessful(statusCode)
}
func (h *routerHandler) subdomainAndPathAndMethodExists(ctx context.Context, t *trie, method, path string) bool { func (h *routerHandler) subdomainAndPathAndMethodExists(ctx context.Context, t *trie, method, path string) bool {
if method != "" && method != t.method { if method != "" && method != t.method {
return false return false

View File

@ -33,6 +33,22 @@ type Party interface {
// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
Macros() *macro.Macros Macros() *macro.Macros
// OnErrorCode registers an error http status code
// based on the "statusCode" < 200 || >= 400 (came from `context.StatusCodeNotSuccessful`).
// The handler is being wrapped by a generic
// handler which will try to reset
// the body if recorder was enabled
// and/or disable the gzip if gzip response recorder
// was active.
OnErrorCode(statusCode int, handlers ...context.Handler)
// OnAnyErrorCode registers a handler which called when error status code written.
// Same as `OnErrorCode` but registers all http error codes based on the `context.StatusCodeNotSuccessful`
// which defaults to < 200 || >= 400 for an error code, any previous error code will be overridden,
// so call it first if you want to use any custom handler for a specific error status code.
//
// Read more at: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
OnAnyErrorCode(handlers ...context.Handler)
// Party groups routes which may have the same prefix and share same handlers, // Party groups routes which may have the same prefix and share same handlers,
// returns that new rich subrouter. // returns that new rich subrouter.
// //

View File

@ -22,6 +22,7 @@ type Route struct {
Name string `json:"name"` // "userRoute" Name string `json:"name"` // "userRoute"
Description string `json:"description"` // "lists a user" Description string `json:"description"` // "lists a user"
Method string `json:"method"` // "GET" Method string `json:"method"` // "GET"
StatusCode int `json:"statusCode"` // 404 (only for HTTP error handlers).
methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one. methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one.
Subdomain string `json:"subdomain"` // "admin." Subdomain string `json:"subdomain"` // "admin."
tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}" tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}"
@ -62,6 +63,9 @@ type Route struct {
LastMod time.Time `json:"lastMod,omitempty"` LastMod time.Time `json:"lastMod,omitempty"`
ChangeFreq string `json:"changeFreq,omitempty"` ChangeFreq string `json:"changeFreq,omitempty"`
Priority float32 `json:"priority,omitempty"` Priority float32 `json:"priority,omitempty"`
// ReadOnly is the read-only structure of the Route.
ReadOnly context.RouteReadOnly
} }
// NewRoute returns a new route based on its method, // NewRoute returns a new route based on its method,
@ -69,7 +73,7 @@ type Route struct {
// handlers and the macro container which all routes should share. // handlers and the macro container which all routes should share.
// It parses the path based on the "macros", // It parses the path based on the "macros",
// handlers are being changed to validate the macros at serve time, if needed. // handlers are being changed to validate the macros at serve time, if needed.
func NewRoute(method, subdomain, unparsedPath string, func NewRoute(statusErrorCode int, method, subdomain, unparsedPath string,
handlers context.Handlers, macros macro.Macros) (*Route, error) { handlers context.Handlers, macros macro.Macros) (*Route, error) {
tmpl, err := macro.Parse(unparsedPath, macros) tmpl, err := macro.Parse(unparsedPath, macros)
if err != nil { if err != nil {
@ -86,9 +90,14 @@ func NewRoute(method, subdomain, unparsedPath string,
path = cleanPath(path) // maybe unnecessary here. path = cleanPath(path) // maybe unnecessary here.
defaultName := method + subdomain + tmpl.Src defaultName := method + subdomain + tmpl.Src
if statusErrorCode > 0 {
defaultName = fmt.Sprintf("%d_%s", statusErrorCode, defaultName)
}
formattedPath := formatPath(path) formattedPath := formatPath(path)
route := &Route{ route := &Route{
StatusCode: statusErrorCode,
Name: defaultName, Name: defaultName,
Method: method, Method: method,
methodBckp: method, methodBckp: method,
@ -99,6 +108,7 @@ func NewRoute(method, subdomain, unparsedPath string,
FormattedPath: formattedPath, FormattedPath: formattedPath,
} }
route.ReadOnly = routeReadOnlyWrapper{route}
return route, nil return route, nil
} }
@ -189,15 +199,20 @@ func (r *Route) BuildHandlers() {
// String returns the form of METHOD, SUBDOMAIN, TMPL PATH. // String returns the form of METHOD, SUBDOMAIN, TMPL PATH.
func (r *Route) String() string { func (r *Route) String() string {
start := r.Method
if r.StatusCode > 0 {
start = http.StatusText(r.StatusCode)
}
return fmt.Sprintf("%s %s%s", return fmt.Sprintf("%s %s%s",
r.Method, r.Subdomain, r.Tmpl().Src) start, r.Subdomain, r.Tmpl().Src)
} }
// Equal compares the method, subdomain and the // Equal compares the method, subdomain and the
// underline representation of the route's path, // underline representation of the route's path,
// instead of the `String` function which returns the front representation. // instead of the `String` function which returns the front representation.
func (r *Route) Equal(other *Route) bool { func (r *Route) Equal(other *Route) bool {
return r.Method == other.Method && r.Subdomain == other.Subdomain && r.Path == other.Path return r.StatusCode == other.StatusCode && r.Method == other.Method && r.Subdomain == other.Subdomain && r.Path == other.Path
} }
// DeepEqual compares the method, subdomain, the // DeepEqual compares the method, subdomain, the
@ -467,6 +482,10 @@ type routeReadOnlyWrapper struct {
*Route *Route
} }
func (rd routeReadOnlyWrapper) StatusErrorCode() int {
return rd.Route.StatusCode
}
func (rd routeReadOnlyWrapper) Method() string { func (rd routeReadOnlyWrapper) Method() string {
return rd.Route.Method return rd.Route.Method
} }

View File

@ -1,161 +0,0 @@
package router
import (
"net/http" // just for status codes
"sync"
"github.com/kataras/iris/v12/context"
)
func statusCodeSuccessful(statusCode int) bool {
return !context.StatusCodeNotSuccessful(statusCode)
}
// ErrorCodeHandler is the entry
// of the list of all http error code handlers.
type ErrorCodeHandler struct {
StatusCode int
Handlers context.Handlers
mu sync.Mutex
}
// Fire executes the specific an error http error status.
// it's being wrapped to make sure that the handler
// will render correctly.
func (ch *ErrorCodeHandler) Fire(ctx context.Context) {
// if we can reset the body
if w, ok := ctx.IsRecording(); ok {
if statusCodeSuccessful(w.StatusCode()) { // if not an error status code
w.WriteHeader(ch.StatusCode) // then set it manually here, otherwise it should be set via ctx.StatusCode(...)
}
// reset if previous content and it's recorder, keep the status code.
w.ClearHeaders()
w.ResetBody()
} else if w, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok {
// reset and disable the gzip in order to be an expected form of http error result
w.ResetBody()
w.Disable()
} else {
// if we can't reset the body and the body has been filled
// which means that the status code already sent,
// then do not fire this custom error code.
if ctx.ResponseWriter().Written() > 0 { // != -1, rel: context/context.go#EndRequest
return
}
}
// ctx.StopExecution() // not uncomment this, is here to remember why to.
// note for me: I don't stopping the execution of the other handlers
// because may the user want to add a fallback error code
// i.e
// users := app.Party("/users")
// users.Done(func(ctx context.Context){ if ctx.StatusCode() == 400 { /* custom error code for /users */ }})
// use .HandlerIndex
// that sets the current handler index to zero
// in order to:
// ignore previous runs that may changed the handler index,
// via ctx.Next or ctx.StopExecution, if any.
//
// use .Do
// that overrides the existing handlers and sets and runs these error handlers.
// in order to:
// ignore the route's after-handlers, if any.
ctx.HandlerIndex(0)
ctx.Do(ch.Handlers)
}
func (ch *ErrorCodeHandler) updateHandlers(handlers context.Handlers) {
ch.mu.Lock()
ch.Handlers = handlers
ch.mu.Unlock()
}
// ErrorCodeHandlers contains the http error code handlers.
// User of this struct can register, get
// a status code handler based on a status code or
// fire based on a receiver context.
type ErrorCodeHandlers struct {
handlers []*ErrorCodeHandler
}
func defaultErrorCodeHandlers() *ErrorCodeHandlers {
chs := new(ErrorCodeHandlers)
// register some common error handlers.
// Note that they can be registered on-fly but
// we don't want to reduce the performance even
// on the first failed request.
for _, statusCode := range []int{
http.StatusNotFound,
http.StatusMethodNotAllowed,
http.StatusInternalServerError,
} {
chs.Register(statusCode, statusText(statusCode))
}
return chs
}
func statusText(statusCode int) context.Handler {
return func(ctx context.Context) {
ctx.WriteString(http.StatusText(statusCode))
}
}
// Get returns an http error handler based on the "statusCode".
// If not found it returns nil.
func (s *ErrorCodeHandlers) Get(statusCode int) *ErrorCodeHandler {
for i, n := 0, len(s.handlers); i < n; i++ {
if h := s.handlers[i]; h.StatusCode == statusCode {
return h
}
}
return nil
}
// Register registers an error http status code
// based on the "statusCode" < 200 || >= 400 (`context.StatusCodeNotSuccessful`).
// The handler is being wrapepd by a generic
// handler which will try to reset
// the body if recorder was enabled
// and/or disable the gzip if gzip response recorder
// was active.
func (s *ErrorCodeHandlers) Register(statusCode int, handlers ...context.Handler) *ErrorCodeHandler {
if statusCodeSuccessful(statusCode) {
return nil
}
h := s.Get(statusCode)
if h == nil {
// create new and add it
ch := &ErrorCodeHandler{
StatusCode: statusCode,
Handlers: handlers,
}
s.handlers = append(s.handlers, ch)
return ch
}
// otherwise update the handlers
h.updateHandlers(handlers)
return h
}
// Fire executes an error http status code handler
// based on the context's status code.
//
// If a handler is not already registered,
// then it creates & registers a new trivial handler on the-fly.
func (s *ErrorCodeHandlers) Fire(ctx context.Context) {
statusCode := ctx.GetStatusCode()
if statusCodeSuccessful(statusCode) {
return
}
ch := s.Get(statusCode)
if ch == nil {
ch = s.Register(statusCode, statusText(statusCode))
}
ch.Fire(ctx)
}

View File

@ -64,9 +64,73 @@ func TestOnAnyErrorCode(t *testing.T) {
} }
func checkAndClearBuf(t *testing.T, buff *bytes.Buffer, expected string) { func checkAndClearBuf(t *testing.T, buff *bytes.Buffer, expected string) {
t.Helper()
if got, expected := buff.String(), expected; got != expected { if got, expected := buff.String(), expected; got != expected {
t.Fatalf("expected middleware to run before the error handler, expected %s but got %s", expected, got) t.Fatalf("expected middleware to run before the error handler, expected: '%s' but got: '%s'", expected, got)
} }
buff.Reset() buff.Reset()
} }
func TestPartyOnErrorCode(t *testing.T) {
app := iris.New()
app.Configure(iris.WithFireMethodNotAllowed)
globalNotFoundResponse := "custom not found"
app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
ctx.WriteString(globalNotFoundResponse)
})
globalMethodNotAllowedResponse := "global: method not allowed"
app.OnErrorCode(iris.StatusMethodNotAllowed, func(ctx iris.Context) {
ctx.WriteString(globalMethodNotAllowedResponse)
})
app.Get("/path", h)
h := func(ctx iris.Context) { ctx.WriteString(ctx.Path()) }
usersResponse := "users: method allowed"
users := app.Party("/users")
users.OnErrorCode(iris.StatusMethodNotAllowed, func(ctx iris.Context) {
ctx.WriteString(usersResponse)
})
users.Get("/", h)
// test setting the error code from a handler.
users.Get("/badrequest", func(ctx iris.Context) { ctx.StatusCode(iris.StatusBadRequest) })
usersuserResponse := "users:user: method allowed"
user := users.Party("/{id:int}")
user.OnErrorCode(iris.StatusMethodNotAllowed, func(ctx iris.Context) {
ctx.WriteString(usersuserResponse)
})
// usersuserNotFoundResponse := "users:user: not found"
// user.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
// ctx.WriteString(usersuserNotFoundResponse)
// })
user.Get("/", h)
e := httptest.New(t, app)
e.GET("/notfound").Expect().Status(iris.StatusNotFound).Body().Equal(globalNotFoundResponse)
e.POST("/path").Expect().Status(iris.StatusMethodNotAllowed).Body().Equal(globalMethodNotAllowedResponse)
e.GET("/path").Expect().Status(iris.StatusOK).Body().Equal("/path")
e.POST("/users").Expect().Status(iris.StatusMethodNotAllowed).
Body().Equal(usersResponse)
e.POST("/users/42").Expect().Status(iris.StatusMethodNotAllowed).
Body().Equal(usersuserResponse)
e.GET("/users/42").Expect().Status(iris.StatusOK).
Body().Equal("/users/42")
// e.GET("/users/ab").Expect().Status(iris.StatusNotFound).Body().Equal(usersuserNotFoundResponse)
// if not registered to the party, then the root is taking action.
e.GET("/users/ab/cd").Expect().Status(iris.StatusNotFound).Body().Equal(globalNotFoundResponse)
// if not registered to the party, and not in root, then just write the status text (fallback behavior)
e.GET("/users/badrequest").Expect().Status(iris.StatusBadRequest).
Body().Equal(http.StatusText(iris.StatusBadRequest))
}

View File

@ -30,8 +30,8 @@ type trieNode struct {
staticKey string staticKey string
// insert data. // insert data.
Handlers context.Handlers Route context.RouteReadOnly
RouteName string Handlers context.Handlers
} }
func newTrieNode() *trieNode { func newTrieNode() *trieNode {
@ -89,7 +89,9 @@ type trie struct {
hasRootWildcard bool hasRootWildcard bool
hasRootSlash bool hasRootSlash bool
method string statusCode int // for error codes only, method is ignored.
method string
// subdomain is empty for default-hostname routes, // subdomain is empty for default-hostname routes,
// ex: mysubdomain. // ex: mysubdomain.
subdomain string subdomain string
@ -108,7 +110,7 @@ func slowPathSplit(path string) []string {
return strings.Split(path, pathSep)[1:] return strings.Split(path, pathSep)[1:]
} }
func (tr *trie) insert(path, routeName string, handlers context.Handlers) { func (tr *trie) insert(path string, route context.RouteReadOnly, handlers context.Handlers) {
input := slowPathSplit(path) input := slowPathSplit(path)
n := tr.root n := tr.root
@ -148,8 +150,9 @@ func (tr *trie) insert(path, routeName string, handlers context.Handlers) {
n = n.getChild(s) n = n.getChild(s)
} }
n.RouteName = routeName n.Route = route
n.Handlers = handlers n.Handlers = handlers
n.paramKeys = paramKeys n.paramKeys = paramKeys
n.key = path n.key = path
n.end = true n.end = true
@ -163,6 +166,8 @@ func (tr *trie) insert(path, routeName string, handlers context.Handlers) {
} }
n.staticKey = path[:i] n.staticKey = path[:i]
// fmt.Printf("trie.insert: (whole path=%v) Path: %s, Route name: %s, Handlers len: %d\n", n.end, n.key, route.Name(), len(handlers))
} }
func (tr *trie) search(q string, params *context.RequestParams) *trieNode { func (tr *trie) search(q string, params *context.RequestParams) *trieNode {

View File

@ -130,7 +130,8 @@ type Application struct {
// routing embedded | exposing APIBuilder's and Router's public API. // routing embedded | exposing APIBuilder's and Router's public API.
*router.APIBuilder *router.APIBuilder
*router.Router *router.Router
ContextPool *context.Pool router.HTTPErrorHandler // if Router is Downgraded this is nil.
ContextPool *context.Pool
// config contains the configuration fields // config contains the configuration fields
// all fields defaults to something that is working, developers don't have to set it. // all fields defaults to something that is working, developers don't have to set it.
@ -834,6 +835,7 @@ func (app *Application) Build() error {
if err != nil { if err != nil {
rp.Err(err) rp.Err(err)
} }
app.HTTPErrorHandler = routerHandler
// re-build of the router from outside can be done with // re-build of the router from outside can be done with
// app.RefreshRouter() // app.RefreshRouter()
} }