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/)
[![Benchmarks: Apr 2, 2020 at 12:13pm (UTC)](https://iris-go.com/images/benchmarks.svg)](https://github.com/kataras/server-benchmarks)
## Aprende Iris
<details>

View File

@ -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)
## آموزش آیریس
<details>

View File

@ -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
<details>

View File

@ -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 배우기
<details>

View File

@ -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
<details>

View File

@ -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
<details>

View File

@ -14,6 +14,7 @@ import (
Build(provider router.RoutesProvider) error
- RouteExists reports whether a particular route exists.
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()

View File

@ -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

View File

@ -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,8 +1189,8 @@ type context struct {
writer ResponseWriter
// the original http.Request
request *http.Request
// the current route's name registered to this request path.
currentRouteName string
// the current route registered to this request path.
currentRoute RouteReadOnly
deferFunc Handler
// the local key-value storage
@ -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.

View File

@ -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

View File

@ -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 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
}
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
@ -165,7 +167,6 @@ var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handl
func NewAPIBuilder() *APIBuilder {
return &APIBuilder{
macros: macro.Defaults,
errorCodeHandlers: defaultErrorCodeHandlers(),
errors: errgroup.New("API Builder"),
relativePath: "/",
routes: new(repository),
@ -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,9 +423,19 @@ 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 {
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.
// but remove the first slash if the relative has already ending with a slash
@ -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.
//

View File

@ -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 {
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
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
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}
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, r.ReadOnly, handlers)
t.insert(path, routeName, handlers)
return nil
}
@ -141,8 +172,12 @@ func (h *routerHandler) Build(provider RoutesProvider) error {
}
if r.Subdomain != "" {
if r.StatusCode > 0 {
h.errorHosts = true
} else {
h.hosts = true
}
}
if r.topLink == nil {
// build the r.Handlers based on begin and done handlers, if any.
@ -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...)
if h.hosts && !h.canHandleSubdomain(ctx, t.subdomain) {
continue
}
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())
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

View File

@ -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.
//

View File

@ -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
}

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) {
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))
}

View File

@ -30,8 +30,8 @@ type trieNode struct {
staticKey string
// insert data.
Route context.RouteReadOnly
Handlers context.Handlers
RouteName string
}
func newTrieNode() *trieNode {
@ -89,7 +89,9 @@ type trie struct {
hasRootWildcard bool
hasRootSlash bool
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 {

View File

@ -130,6 +130,7 @@ type Application struct {
// routing embedded | exposing APIBuilder's and Router's public API.
*router.APIBuilder
*router.Router
router.HTTPErrorHandler // if Router is Downgraded this is nil.
ContextPool *context.Pool
// config contains the configuration fields
@ -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()
}