diff --git a/README_ES.md b/README_ES.md index c9a7696b..0a7f289d 100644 --- a/README_ES.md +++ b/README_ES.md @@ -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/) +[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks) + ## Aprende Iris
diff --git a/README_FA.md b/README_FA.md index d6892b13..2af2c7a3 100644 --- a/README_FA.md +++ b/README_FA.md @@ -12,6 +12,8 @@ [![](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) + ## آموزش آیریس
diff --git a/README_GR.md b/README_GR.md index 4045a586..035f65e4 100644 --- a/README_GR.md +++ b/README_GR.md @@ -8,6 +8,8 @@ [![](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
diff --git a/README_KO.md b/README_KO.md index a686c62f..496312a0 100644 --- a/README_KO.md +++ b/README_KO.md @@ -8,6 +8,8 @@ Iris는 단순하고 빠르며 좋은 성능과 모든 기능을 갖춘 Go언어 [![](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 배우기
diff --git a/README_RU.md b/README_RU.md index 37543790..51ce4a01 100644 --- a/README_RU.md +++ b/README_RU.md @@ -7,6 +7,8 @@ Iris — это быстрый, простой, но полнофункцион [![](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
diff --git a/README_ZH.md b/README_ZH.md index ec7fc261..a0fecfb0 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -8,6 +8,8 @@ Iris 是基于 Go 编写的一个快速,简单但功能齐全且非常高效 [![](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
diff --git a/_examples/routing/custom-high-level-router/main.go b/_examples/routing/custom-high-level-router/main.go index 764daab4..5d9e8e4b 100644 --- a/_examples/routing/custom-high-level-router/main.go +++ b/_examples/routing/custom-high-level-router/main.go @@ -13,7 +13,8 @@ import ( - Build should builds the handler, it's being called on router's BuildRouter. Build(provider router.RoutesProvider) error - 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 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) return } @@ -71,6 +72,11 @@ func (r *customRouter) RouteExists(ctx iris.Context, method, path string) bool { return false } +func (r *customRouter) FireErrorCode(ctx iris.Context) { + // responseStatusCode := ctx.GetStatusCode() // set by prior ctx.StatusCode calls + // [...] +} + func main() { app := iris.New() diff --git a/context/application.go b/context/application.go index 5f760c08..8ba980bb 100644 --- a/context/application.go +++ b/context/application.go @@ -61,9 +61,6 @@ type Application interface { // FireErrorCode 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. FireErrorCode(ctx Context) // RouteExists reports whether a particular route exists diff --git a/context/context.go b/context/context.go index b39672d8..89dd6669 100644 --- a/context/context.go +++ b/context/context.go @@ -138,18 +138,13 @@ type Context interface { // ctx.ResetRequest(r.WithContext(stdCtx)). ResetRequest(r *http.Request) - // SetCurrentRouteName sets the route's name internally, - // in order to be able to find the correct current "read-only" Route when - // end-developer calls the `GetCurrentRoute()` function. - // It's being initialized by the Router, if you change that name - // manually nothing really happens except that you'll get other - // route via `GetCurrentRoute()`. - // Instead, to execute a different 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. + // SetCurrentRoutes sets the route internally, + // See `GetCurrentRoute()` method too. + // It's being initialized by the Router. + // See `Exec` or `SetHandlers/AddHandler` methods to simulate a request. + SetCurrentRoute(route RouteReadOnly) + // GetCurrentRoute returns the current "read-only" route that + // was registered to this request's path. GetCurrentRoute() RouteReadOnly // Do calls the SetHandlers(handlers) @@ -175,7 +170,7 @@ type Context interface { // HandlerIndex sets the current index of the // 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. // // Look Handlers(), Next() and StopExecution() too. @@ -1194,9 +1189,9 @@ type context struct { writer ResponseWriter // the original http.Request request *http.Request - // the current route's name registered to this request path. - currentRouteName string - deferFunc Handler + // the current route registered to this request path. + currentRoute RouteReadOnly + deferFunc Handler // the local key-value storage params RequestParams // url named parameters. @@ -1230,6 +1225,7 @@ func NewContext(app Application) Context { // 4. response writer to the http.ResponseWriter. // 5. request to the *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.values = ctx.values[0:0] // >> >> by context.Values().Set ctx.params.Store = ctx.params.Store[0:0] @@ -1266,8 +1262,8 @@ func (ctx *context) EndRequest() { ctx.deferFunc(ctx) } - if StatusCodeNotSuccessful(ctx.GetStatusCode()) && - !ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() { + if !ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() && + StatusCodeNotSuccessful(ctx.GetStatusCode()) { // author's note: // if recording, the error handler can handle // the rollback and remove any response written before, @@ -1332,23 +1328,18 @@ func (ctx *context) ResetRequest(r *http.Request) { ctx.request = r } -// SetCurrentRouteName sets the route's name internally, -// in order to be able to find the correct current "read-only" Route when -// end-developer calls the `GetCurrentRoute()` function. -// It's being initialized by the Router, if you change that name -// manually nothing really happens except that you'll get other -// route via `GetCurrentRoute()`. -// 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 +// SetCurrentRoutes sets the route internally, +// See `GetCurrentRoute()` method too. +// It's being initialized by the Router. +// See `Exec` or `SetHandlers/AddHandler` methods to simulate a request. +func (ctx *context) SetCurrentRoute(route RouteReadOnly) { + ctx.currentRoute = route } -// GetCurrentRoute returns the current registered "read-only" route that -// was being registered to this request's path. +// GetCurrentRoute returns the current "read-only" route that +// was registered to this request's path. func (ctx *context) GetCurrentRoute() RouteReadOnly { - return ctx.app.GetRouteReadOnly(ctx.currentRouteName) + return ctx.currentRoute } // Do calls the SetHandlers(handlers) @@ -1385,8 +1376,8 @@ func (ctx *context) Handlers() Handlers { // HandlerIndex sets the current index of the // current context's handlers chain. -// If -1 passed then it just returns the -// current handler index without change the current index.rns that index, useless return value. +// If n < 0 or the current handlers length is 0 then it just returns the +// current handler index without change the current index. // // Look Handlers(), Next() and StopExecution() too. 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. -// 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 { - return ctx.currentRouteName + if ctx.currentRoute == nil { + return "" + } + + return ctx.currentRoute.Name() } // Next is the function that executed when `ctx.Next()` is called. diff --git a/context/route.go b/context/route.go index 2c4b2eed..dcce5f36 100644 --- a/context/route.go +++ b/context/route.go @@ -17,6 +17,10 @@ type RouteReadOnly interface { // Name returns the route's name. 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() string diff --git a/core/router/api_builder.go b/core/router/api_builder.go index c24b5a55..d9052d6e 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -58,7 +58,7 @@ func (repo *repository) getRelative(r *Route) *Route { } 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 } } @@ -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) - 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 } @@ -117,8 +121,6 @@ type APIBuilder struct { // the api builder global macros registry 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 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. func NewAPIBuilder() *APIBuilder { return &APIBuilder{ - macros: macro.Defaults, - errorCodeHandlers: defaultErrorCodeHandlers(), - errors: errgroup.New("API Builder"), - relativePath: "/", - routes: new(repository), - apiBuilderDI: &APIContainer{Container: hero.New()}, + macros: macro.Defaults, + errors: errgroup.New("API Builder"), + relativePath: "/", + routes: new(repository), + 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. 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 err error @@ -282,6 +287,7 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co if route == nil { break } + // global 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 // 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 { - if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any - return api.Any(relativePath, handlers...) + return api.createRoutes(0, methods, 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. @@ -444,11 +460,17 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl // So if we just put `api.middleware` or `api.doneHandlers` // 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. + var ( - beginHandlers = joinHandlers(api.middleware, context.Handlers{}) - doneHandlers = joinHandlers(api.doneHandlers, context.Handlers{}) + beginHandlers context.Handlers + doneHandlers context.Handlers ) + if errorCode == 0 { + beginHandlers = joinHandlers(api.middleware, beginHandlers) + doneHandlers = joinHandlers(api.doneHandlers, doneHandlers) + } + mainHandlers := context.Handlers(handlers) // 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)) - for i, m := range methods { - route, err := NewRoute(m, subdomain, path, routeHandlers, *api.macros) + for i, m := range methods { // single, empty method for error handlers. + route, err := NewRoute(errorCode, m, subdomain, path, routeHandlers, *api.macros) if err != nil { // template path parser errors: api.errors.Addf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path) continue @@ -523,6 +545,13 @@ func removeDuplicates(elements []string) (result []string) { // // You can even declare a subdomain with relativePath as "mysub." or see `Subdomain`. 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 dot := string(SubdomainPrefix[0]) 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 macros: api.macros, routes: api.routes, - errorCodeHandlers: api.errorCodeHandlers, beginGlobalHandlers: api.beginGlobalHandlers, doneGlobalHandlers: api.doneGlobalHandlers, errors: api.errors, @@ -674,7 +702,7 @@ func (api *APIBuilder) GetRoutesReadOnly() []context.RouteReadOnly { routes := api.GetRoutes() readOnlyRoutes := make([]context.RouteReadOnly, len(routes)) for i, r := range routes { - readOnlyRoutes[i] = routeReadOnlyWrapper{r} + readOnlyRoutes[i] = r.ReadOnly } return readOnlyRoutes @@ -692,7 +720,7 @@ func (api *APIBuilder) GetRouteReadOnly(routeName string) context.RouteReadOnly if r == nil { return nil } - return routeReadOnlyWrapper{r} + return r.ReadOnly } // 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 routeReadOnlyWrapper{r} + return r.ReadOnly } // 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 // was active. func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) { - if len(api.beginGlobalHandlers) > 0 { - handlers = joinHandlers(api.beginGlobalHandlers, handlers) + // TODO: think a stable way for that and document it so end-developers + // 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. @@ -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. // It returns the current Party. // diff --git a/core/router/handler.go b/core/router/handler.go index 4f328fd3..af69b017 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -15,26 +15,42 @@ import ( "github.com/kataras/pio" ) -// RequestHandler the middle man between acquiring a context and releasing it. -// By-default is the router algorithm. -type RequestHandler interface { - // 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 -} +type ( + // RequestHandler the middle man between acquiring a context and releasing it. + // By-default is the router algorithm. + RequestHandler interface { + // Note: A different interface in order to hide the rest of the implementation. + // We only need the `FireErrorCode` to be accessible through the Iris application (see `iris.go#Build`) + HTTPErrorHandler + + // 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 { config context.ConfigurationReadOnly logger *golog.Logger - trees []*trie - hosts bool // true if at least one route contains a Subdomain. + trees []*trie + 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 // 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 { t := h.trees[i] 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`. func (h *routerHandler) AddRoute(r *Route) error { var ( - routeName = r.Name - method = r.Method - subdomain = r.Subdomain - path = r.Path - handlers = r.Handlers + method = r.Method + statusCode = r.StatusCode + subdomain = r.Subdomain + path = r.Path + handlers = r.Handlers ) - t := h.getTree(method, subdomain) + t := h.getTree(statusCode, method, subdomain) if t == nil { n := newTrieNode() // first time we register a route to this method with this subdomain - t = &trie{method: method, subdomain: subdomain, root: n} - h.trees = append(h.trees, t) + t = &trie{statusCode: statusCode, method: method, subdomain: subdomain, root: n} + 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 } @@ -141,7 +172,11 @@ func (h *routerHandler) Build(provider RoutesProvider) error { } if r.Subdomain != "" { - h.hosts = true + if r.StatusCode > 0 { + h.errorHosts = true + } else { + h.hosts = true + } } 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. } + currentStatusCode := r.StatusCode + if currentStatusCode == 0 { + currentStatusCode = http.StatusOK + } + decisionHandler := func(ctx context.Context) { // 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}, // because the handler on this case is executing from last to top. if f(ctx) { // 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.Do(h) return } - ctx.SetCurrentRouteName(currentRouteName) - ctx.StatusCode(http.StatusOK) + ctx.SetCurrentRoute(currentRoute) + ctx.StatusCode(currentStatusCode) ctx.Next() } 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) { method := ctx.Method() path := ctx.Path() @@ -304,40 +384,13 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { 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 := 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 - } + if h.hosts && !h.canHandleSubdomain(ctx, t.subdomain) { + continue } + n := t.search(path, ctx.Params()) if n != nil { - ctx.SetCurrentRouteName(n.RouteName) + ctx.SetCurrentRoute(n.Route) ctx.Do(n.Handlers) // found return @@ -364,6 +417,89 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { 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 { if method != "" && method != t.method { return false diff --git a/core/router/party.go b/core/router/party.go index 74e82566..dec3c279 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -33,6 +33,22 @@ type Party interface { // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path 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, // returns that new rich subrouter. // diff --git a/core/router/route.go b/core/router/route.go index b89650ad..0b18eabd 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -22,6 +22,7 @@ type Route struct { Name string `json:"name"` // "userRoute" Description string `json:"description"` // "lists a user" 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. Subdomain string `json:"subdomain"` // "admin." tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}" @@ -62,6 +63,9 @@ type Route struct { LastMod time.Time `json:"lastMod,omitempty"` ChangeFreq string `json:"changeFreq,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, @@ -69,7 +73,7 @@ type Route struct { // handlers and the macro container which all routes should share. // It parses the path based on the "macros", // 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) { tmpl, err := macro.Parse(unparsedPath, macros) if err != nil { @@ -86,9 +90,14 @@ func NewRoute(method, subdomain, unparsedPath string, path = cleanPath(path) // maybe unnecessary here. defaultName := method + subdomain + tmpl.Src + if statusErrorCode > 0 { + defaultName = fmt.Sprintf("%d_%s", statusErrorCode, defaultName) + } + formattedPath := formatPath(path) route := &Route{ + StatusCode: statusErrorCode, Name: defaultName, Method: method, methodBckp: method, @@ -99,6 +108,7 @@ func NewRoute(method, subdomain, unparsedPath string, FormattedPath: formattedPath, } + route.ReadOnly = routeReadOnlyWrapper{route} return route, nil } @@ -189,15 +199,20 @@ func (r *Route) BuildHandlers() { // String returns the form of METHOD, SUBDOMAIN, TMPL PATH. func (r *Route) String() string { + start := r.Method + if r.StatusCode > 0 { + start = http.StatusText(r.StatusCode) + } + 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 // underline representation of the route's path, // instead of the `String` function which returns the front representation. 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 @@ -467,6 +482,10 @@ type routeReadOnlyWrapper struct { *Route } +func (rd routeReadOnlyWrapper) StatusErrorCode() int { + return rd.Route.StatusCode +} + func (rd routeReadOnlyWrapper) Method() string { return rd.Route.Method } diff --git a/core/router/status.go b/core/router/status.go deleted file mode 100644 index bb4464ee..00000000 --- a/core/router/status.go +++ /dev/null @@ -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) -} diff --git a/core/router/status_test.go b/core/router/status_test.go index e55a0cde..f2f9a7b4 100644 --- a/core/router/status_test.go +++ b/core/router/status_test.go @@ -64,9 +64,73 @@ func TestOnAnyErrorCode(t *testing.T) { } func checkAndClearBuf(t *testing.T, buff *bytes.Buffer, expected string) { + t.Helper() + 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() } + +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)) +} diff --git a/core/router/trie.go b/core/router/trie.go index c24b8ba5..a22dc769 100644 --- a/core/router/trie.go +++ b/core/router/trie.go @@ -30,8 +30,8 @@ type trieNode struct { staticKey string // insert data. - Handlers context.Handlers - RouteName string + Route context.RouteReadOnly + Handlers context.Handlers } func newTrieNode() *trieNode { @@ -89,7 +89,9 @@ type trie struct { hasRootWildcard bool hasRootSlash bool - method string + statusCode int // for error codes only, method is ignored. + method string + // subdomain is empty for default-hostname routes, // ex: mysubdomain. subdomain string @@ -108,7 +110,7 @@ func slowPathSplit(path string) []string { 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) n := tr.root @@ -148,8 +150,9 @@ func (tr *trie) insert(path, routeName string, handlers context.Handlers) { n = n.getChild(s) } - n.RouteName = routeName + n.Route = route n.Handlers = handlers + n.paramKeys = paramKeys n.key = path n.end = true @@ -163,6 +166,8 @@ func (tr *trie) insert(path, routeName string, handlers context.Handlers) { } 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 { diff --git a/iris.go b/iris.go index ab70fecb..38ce8002 100644 --- a/iris.go +++ b/iris.go @@ -130,7 +130,8 @@ type Application struct { // routing embedded | exposing APIBuilder's and Router's public API. *router.APIBuilder *router.Router - ContextPool *context.Pool + router.HTTPErrorHandler // if Router is Downgraded this is nil. + ContextPool *context.Pool // config contains the configuration fields // 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 { rp.Err(err) } + app.HTTPErrorHandler = routerHandler // re-build of the router from outside can be done with // app.RefreshRouter() }