Offling routing and prioritize others before static handlers https://github.com/kataras/iris/issues/585

Read HISTORY.md
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-01-12 10:24:27 +02:00
parent c91a1e6628
commit 3489ba3365
6 changed files with 227 additions and 86 deletions

View File

@ -8,10 +8,14 @@
- Discussion: https://github.com/kataras/iris/issues/585
- Test: https://github.com/kataras/iris/blob/master/http_test.go#L735
- Example: https://github.com/iris-contrib/examples/tree/master/route_state
- Example 1: https://github.com/iris-contrib/examples/tree/master/route_state
- Example 2, SPA: https://github.com/iris-contrib/examples/tree/master/spa_2_using_offline_routing
**What?**
- Give priority to an API path inside a Static route
```go
package main
@ -21,38 +25,57 @@ import (
func main() {
iris.None("/api/user/:userid", func(ctx *iris.Context) {
usersAPI := iris.None("/api/users/:userid", func(ctx *iris.Context) {
ctx.Writef("user with id: %s", ctx.Param("userid"))
})("api.users.id")
iris.StaticWeb("/", "./www", usersAPI)
//
// START THE SERVER
//
iris.Listen("localhost:8080")
}
```
- Play with(very advanced usage, used by big companies): enable(online) or disable(offline) routes at runtime with one line of code.
```go
package main
import (
"github.com/kataras/iris"
)
func main() {
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
api := iris.None("/api/users/:userid", func(ctx *iris.Context) {
userid := ctx.Param("userid")
ctx.Writef("user with id: %s", userid)
})("user.api")
})("users.api")
// change the "user.api" state from offline to online and online to offline
// change the "users.api" state from offline to online and online to offline
iris.Get("/change", func(ctx *iris.Context) {
routeName := "user.api"
if iris.Lookup(routeName).IsOnline() {
if api.IsOnline() {
// set to offline
iris.SetRouteOffline(routeName)
iris.SetRouteOffline(api)
} else {
// set to online if it was not online(so it was offline)
iris.SetRouteOnline(routeName, iris.MethodGet)
iris.SetRouteOnline(api, iris.MethodGet)
}
})
// iris.Get("/execute/:routename", func(ctx *iris.Context) {
// routeName := ctx.Param("routename")
// userAPICtx := ctx.ExecuteRoute(routeName)
// if userAPICtx == nil {
// ctx.Writef("Route with name: %s didnt' found or couldn't be validate with this request path!", routeName)
// }
// })
iris.Get("/execute", func(ctx *iris.Context) {
routeName := "user.api"
// change the path in order to be catcable from the ExecuteRoute
// ctx.Request.URL.Path = "/api/user/42"
// ctx.ExecRoute(routeName)
// ctx.Request.URL.Path = "/api/users/42"
// ctx.ExecRoute(iris.Route)
// or:
ctx.ExecRouteAgainst(routeName, "/api/user/42")
ctx.ExecRouteAgainst(api, "/api/users/42")
})
iris.Get("/", func(ctx *iris.Context) {
@ -63,11 +86,11 @@ func main() {
// START THE SERVER
//
// STEPS:
// 1. navigate to http://localhost:8080/user/api/42
// 1. navigate to http://localhost:8080/api/users/42
// you should get 404 error
// 2. now, navigate to http://localhost:8080/change
// you should see a blank page
// 3. now, navigate to http://localhost:8080/user/api/42
// 3. now, navigate to http://localhost:8080/api/users/42
// you should see the page working, NO 404 error
// go back to the http://localhost:8080/change
// you should get 404 error again
@ -78,6 +101,25 @@ func main() {
```
- New built'n Middleware: `iris.Prioritize(route)` in order to give priority to a route inside other handler (used internally on StaticWeb's builder)
```go
usersAPI := iris.None("/api/users/:userid", func(ctx *iris.Context) {
ctx.Writef("user with id: %s", ctx.Param("userid"))
})("api.users.id") // we need to call empty ("") in order to get its iris.Route instance
// or ("the name of the route")
// which later on can be found with iris.Lookup("the name of the route")
static := iris.StaticHandler("/", "./www", false, false)
// manually give a priority to the usersAPI, if not found then continue to the static handler
iris.Get("/*file", iris.Prioritize(usersAPI), static)
iris.Get("/*file", static)
iris.Listen(":8080")
```
## 6.0.9 -> 6.1.0
- Fix a not found error when serving static files through custom subdomain, this should work again: `iris.Party("mysubdomain.").StaticWeb("/", "./static")`

View File

@ -177,11 +177,15 @@ func (ctx *Context) GetHandlerName() string {
return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name()
}
// ExecRoute calls any route by its name (mostly "offline" route) like it was requested by the user, but it is not.
// ExecRoute calls any route (mostly "offline" route) like it was requested by the user, but it is not.
// Offline means that the route is registered to the iris and have all features that a normal route has
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
// it can validate paths, has sessions, path parameters and all.
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// It doesn't changes the global state, if a route was "offline" it remains offline.
//
// see ExecRouteAgainst(routeName, againstRequestPath string),
@ -189,16 +193,20 @@ func (ctx *Context) GetHandlerName() string {
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func (ctx *Context) ExecRoute(routeName string) *Context {
return ctx.ExecRouteAgainst(routeName, ctx.Path())
func (ctx *Context) ExecRoute(r Route) *Context {
return ctx.ExecRouteAgainst(r, ctx.Path())
}
// ExecRouteAgainst calls any route by its name (mostly "offline" route) against a 'virtually' request path
// ExecRouteAgainst calls any iris.Route against a 'virtually' request path
// like it was requested by the user, but it is not.
// Offline means that the route is registered to the iris and have all features that a normal route has
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
// it can validate paths, has sessions, path parameters and all.
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// It doesn't changes the global state, if a route was "offline" it remains offline.
//
// see ExecRoute(routeName),
@ -206,9 +214,7 @@ func (ctx *Context) ExecRoute(routeName string) *Context {
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func (ctx *Context) ExecRouteAgainst(routeName string, againstRequestPath string) *Context {
r := ctx.framework.Lookup(routeName)
func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Context {
if r != nil {
context := &(*ctx)
context.Middleware = context.Middleware[0:0]
@ -220,10 +226,42 @@ func (ctx *Context) ExecRouteAgainst(routeName string, againstRequestPath string
return context
}
}
// if failed return nil in order to this fail to be catchable
return nil
}
// Prioritize is a middleware which executes a route against this path
// when the request's Path has a prefix of the route's STATIC PART
// is not executing ExecRoute to determinate if it's valid, for performance reasons
// if this function is not enough for you and you want to test more than one parameterized path
// then use the: if c := ExecRoute(r); c == nil { /* move to the next, the route is not valid */ }
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
// if the route found then it executes that and don't continue to the next handler
// if not found then continue to the next handler
func Prioritize(r Route) HandlerFunc {
if r != nil {
return func(ctx *Context) {
reqPath := ctx.Path()
if strings.HasPrefix(reqPath, r.StaticPath()) {
newctx := ctx.ExecRouteAgainst(r, reqPath)
if newctx == nil { // route not found.
ctx.EmitError(StatusNotFound)
}
return
}
// execute the next handler if no prefix
// here look, the only error we catch is the 404,
// we can't go ctx.Next() and believe that the next handler will manage the error
// because it will not, we are not on the router.
ctx.Next()
}
}
return func(ctx *Context) { ctx.Next() }
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -----------------------------Request URL, Method, IP & Headers getters---------------

32
http.go
View File

@ -698,8 +698,16 @@ type (
Subdomain() string
// Method returns the http method
Method() string
// SetMethod sets the route's method
// requires re-build of the iris.Router
SetMethod(string)
// Path returns the path
Path() string
// staticPath returns the static part of the path
StaticPath() string
// SetPath changes/sets the path for this route
SetPath(string)
// Middleware returns the slice of Handler([]Handler) registered to this route
@ -716,6 +724,7 @@ type (
subdomain string
method string
path string
staticPath string
middleware Middleware
formattedPath string
formattedParts int
@ -740,6 +749,7 @@ var _ Route = &route{}
func newRoute(method string, subdomain string, path string, middleware Middleware) *route {
r := &route{name: path + subdomain, method: method, subdomain: subdomain, path: path, middleware: middleware}
r.formatPath()
r.calculateStaticPath()
return r
}
@ -773,8 +783,20 @@ func (r *route) formatPath() {
r.formattedPath = tempPath
}
func (r *route) setName(newName string) {
func (r *route) calculateStaticPath() {
for i := 0; i < len(r.path); i++ {
if r.path[i] == matchEverythingByte || r.path[i] == parameterStartByte {
r.staticPath = r.path[0 : i-1] // stop at the first dynamic path symbol and set the static path to its [0:previous]
return
}
}
// not a dynamic symbol found, set its static path to its path.
r.staticPath = r.path
}
func (r *route) setName(newName string) Route {
r.name = newName
return r
}
func (r route) Name() string {
@ -789,10 +811,18 @@ func (r route) Method() string {
return r.method
}
func (r *route) SetMethod(method string) {
r.method = method
}
func (r route) Path() string {
return r.path
}
func (r route) StaticPath() string {
return r.staticPath
}
func (r *route) SetPath(s string) {
r.path = s
}

View File

@ -734,36 +734,34 @@ func TestRedirectHTTPS(t *testing.T) {
func TestRouteStateSimple(t *testing.T) {
iris.ResetDefault()
// here
offlineRouteName := "user.api"
offlineRoutePath := "/api/user/:userid"
offlineRouteRequestedTestPath := "/api/user/42"
offlineBody := "user with id: 42"
iris.None(offlineRoutePath, func(ctx *iris.Context) {
offlineRoute := iris.None(offlineRoutePath, func(ctx *iris.Context) {
userid := ctx.Param("userid")
if userid != "42" {
// we are expecting userid 42 always in this test so
t.Fatalf("what happened? expected userid to be 42 but got %s", userid)
}
ctx.Writef(offlineBody)
})(offlineRouteName)
})("api.users") // or an empty (), required, in order to get the Route instance.
// change the "user.api" state from offline to online and online to offline
iris.Get("/change", func(ctx *iris.Context) {
// here
if iris.Lookup(offlineRouteName).IsOnline() {
if offlineRoute.IsOnline() {
// set to offline
iris.SetRouteOffline(offlineRouteName)
iris.SetRouteOffline(offlineRoute)
} else {
// set to online if it was not online(so it was offline)
iris.SetRouteOnline(offlineRouteName, iris.MethodGet)
iris.SetRouteOnline(offlineRoute, iris.MethodGet)
}
})
iris.Get("/execute", func(ctx *iris.Context) {
// here
ctx.ExecRouteAgainst(offlineRouteName, "/api/user/42")
ctx.ExecRouteAgainst(offlineRoute, "/api/user/42")
})
hello := "Hello from index"

82
iris.go
View File

@ -171,9 +171,9 @@ type (
Lookup(routeName string) Route
Lookups() []Route
SetRouteOnline(routeName string, HTTPMethod string) bool
SetRouteOffline(routeName string) bool
ChangeRouteState(routeName string, HTTPMethod string) bool
SetRouteOnline(r Route, HTTPMethod string) bool
SetRouteOffline(r Route) bool
ChangeRouteState(r Route, HTTPMethod string) bool
Path(routeName string, optionalPathParameters ...interface{}) (routePath string)
URL(routeName string, optionalPathParameters ...interface{}) (routeURL string)
@ -219,8 +219,8 @@ type (
StaticEmbedded(reqRelativePath string, contentType string, assets func(string) ([]byte, error), assetsNames func() []string) RouteNameFunc
Favicon(systemFilePath string, optionalReqRelativePath ...string) RouteNameFunc
// static file system
StaticHandler(reqRelativePath string, systemPath string, showList bool, enableGzip bool) HandlerFunc
StaticWeb(reqRelativePath string, systemPath string) RouteNameFunc
StaticHandler(reqRelativePath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...Route) HandlerFunc
StaticWeb(reqRelativePath string, systemPath string, exceptRoutes ...Route) RouteNameFunc
// party layout for template engines
Layout(layoutTemplateFileName string) MuxAPI
@ -231,7 +231,12 @@ type (
}
// RouteNameFunc the func returns from the MuxAPi's methods, optionally sets the name of the Route (*route)
RouteNameFunc func(customRouteName string)
//
// You can find the Route by iris.Lookup("theRouteName")
// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName")
// that will set a name to the route and returns its iris.Route instance for further usage.
//
RouteNameFunc func(customRouteName string) Route
)
// Framework is our God |\| Google.Search('Greek mythology Iris')
@ -1060,8 +1065,8 @@ func (s *Framework) Lookups() (routes []Route) {
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func SetRouteOnline(routeName string, HTTPMethod string) bool {
return Default.SetRouteOnline(routeName, HTTPMethod)
func SetRouteOnline(r Route, HTTPMethod string) bool {
return Default.SetRouteOnline(r, HTTPMethod)
}
// SetRouteOffline sets the state of the route to "offline" and re-builds the router
@ -1073,8 +1078,8 @@ func SetRouteOnline(routeName string, HTTPMethod string) bool {
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func SetRouteOffline(routeName string) bool {
return Default.SetRouteOffline(routeName)
func SetRouteOffline(r Route) bool {
return Default.SetRouteOffline(r)
}
// ChangeRouteState changes the state of the route.
@ -1089,23 +1094,23 @@ func SetRouteOffline(routeName string) bool {
// For more details look: https://github.com/kataras/iris/issues/585
//
// Example: https://github.com/iris-contrib/examples/tree/master/route_state
func ChangeRouteState(routeName string, HTTPMethod string) bool {
return Default.ChangeRouteState(routeName, HTTPMethod)
func ChangeRouteState(r Route, HTTPMethod string) bool {
return Default.ChangeRouteState(r, HTTPMethod)
}
// SetRouteOnline sets the state of the route to "online" with a specific http method
// it re-builds the router
//
// returns true if state was actually changed
func (s *Framework) SetRouteOnline(routeName string, HTTPMethod string) bool {
return s.ChangeRouteState(routeName, HTTPMethod)
func (s *Framework) SetRouteOnline(r Route, HTTPMethod string) bool {
return s.ChangeRouteState(r, HTTPMethod)
}
// SetRouteOffline sets the state of the route to "offline" and re-builds the router
//
// returns true if state was actually changed
func (s *Framework) SetRouteOffline(routeName string) bool {
return s.ChangeRouteState(routeName, MethodNone)
func (s *Framework) SetRouteOffline(r Route) bool {
return s.ChangeRouteState(r, MethodNone)
}
// ChangeRouteState changes the state of the route.
@ -1114,15 +1119,14 @@ func (s *Framework) SetRouteOffline(routeName string) bool {
// it re-builds the router
//
// returns true if state was actually changed
func (s *Framework) ChangeRouteState(routeName string, HTTPMethod string) bool {
r := s.mux.lookup(routeName)
nonSpecificMethod := len(HTTPMethod) == 0
func (s *Framework) ChangeRouteState(r Route, HTTPMethod string) bool {
if r != nil {
if r.method != HTTPMethod {
nonSpecificMethod := len(HTTPMethod) == 0
if r.Method() != HTTPMethod {
if nonSpecificMethod {
r.method = MethodGet // if no method given, then do it for "GET" only
r.SetMethod(MethodGet) // if no method given, then do it for "GET" only
} else {
r.method = HTTPMethod
r.SetMethod(HTTPMethod)
}
// re-build the router/main handler
s.Router = ToNativeHandler(s, s.mux.BuildHandler())
@ -2064,32 +2068,13 @@ func (api *muxAPI) Favicon(favPath string, requestPath ...string) RouteNameFunc
return api.registerResourceRoute(reqPath, h)
}
// StripPrefix returns a handler that serves HTTP requests
// by removing the given prefix from the request URL's Path
// and invoking the handler h. StripPrefix handles a
// request for a path that doesn't begin with prefix by
// replying with an HTTP 404 not found error.
func StripPrefix(prefix string, h HandlerFunc) HandlerFunc {
if prefix == "" {
return h
}
return func(ctx *Context) {
if p := strings.TrimPrefix(ctx.Request.URL.Path, prefix); len(p) < len(ctx.Request.URL.Path) {
ctx.Request.URL.Path = p
h(ctx)
} else {
ctx.NotFound()
}
}
}
// StaticHandler returns a new Handler which serves static files
func StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool) HandlerFunc {
return Default.StaticHandler(reqPath, systemPath, showList, enableGzip)
}
// StaticHandler returns a new Handler which serves static files
func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool) HandlerFunc {
func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...Route) HandlerFunc {
// here we separate the path from the subdomain (if any), we care only for the path
// fixes a bug when serving static files via a subdomain
fullpath := api.relativePath + reqPath
@ -2102,6 +2087,7 @@ func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList boo
Path(path).
Listing(showList).
Gzip(enableGzip).
Except(exceptRoutes...).
Build()
managedStaticHandler := func(ctx *Context) {
@ -2124,6 +2110,8 @@ func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList boo
//
// first parameter: the route path
// second parameter: the system directory
// third OPTIONAL parameter: the exception routes
// (= give priority to these routes instead of the static handler)
// for more options look iris.StaticHandler.
//
// iris.StaticWeb("/static", "./static")
@ -2133,8 +2121,8 @@ func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList boo
// "index.html".
//
// StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ).
func StaticWeb(reqPath string, systemPath string) RouteNameFunc {
return Default.StaticWeb(reqPath, systemPath)
func StaticWeb(reqPath string, systemPath string, exceptRoutes ...Route) RouteNameFunc {
return Default.StaticWeb(reqPath, systemPath, exceptRoutes...)
}
// StaticWeb returns a handler that serves HTTP requests
@ -2142,6 +2130,8 @@ func StaticWeb(reqPath string, systemPath string) RouteNameFunc {
//
// first parameter: the route path
// second parameter: the system directory
// third OPTIONAL parameter: the exception routes
// (= give priority to these routes instead of the static handler)
// for more options look iris.StaticHandler.
//
// iris.StaticWeb("/static", "./static")
@ -2151,8 +2141,8 @@ func StaticWeb(reqPath string, systemPath string) RouteNameFunc {
// "index.html".
//
// StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ).
func (api *muxAPI) StaticWeb(reqPath string, systemPath string) RouteNameFunc {
h := api.StaticHandler(reqPath, systemPath, false, false)
func (api *muxAPI) StaticWeb(reqPath string, systemPath string, exceptRoutes ...Route) RouteNameFunc {
h := api.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...)
routePath := validateWildcard(reqPath, "file")
return api.registerResourceRoute(routePath, h)
}

View File

@ -14,6 +14,7 @@ type StaticHandlerBuilder interface {
Gzip(enable bool) StaticHandlerBuilder
Listing(listDirectoriesOnOff bool) StaticHandlerBuilder
StripPath(yesNo bool) StaticHandlerBuilder
Except(r ...Route) StaticHandlerBuilder
Build() HandlerFunc
}
@ -27,6 +28,7 @@ type webfs struct {
// these are init on the Build() call
filesystem http.FileSystem
once sync.Once
exceptions []Route
handler HandlerFunc
}
@ -88,6 +90,13 @@ func (w *webfs) StripPath(yesNo bool) StaticHandlerBuilder {
return w
}
// Except add a route exception,
// gives priority to that Route over the static handler.
func (w *webfs) Except(r ...Route) StaticHandlerBuilder {
w.exceptions = append(w.exceptions, r...)
return w
}
type (
noListFile struct {
http.File
@ -130,7 +139,7 @@ func (w *webfs) Build() HandlerFunc {
fsHandler = http.StripPrefix(prefix, fileserver)
}
w.handler = func(ctx *Context) {
h := func(ctx *Context) {
writer := ctx.ResponseWriter
if w.gzip && ctx.clientAllowsGzip() {
@ -143,7 +152,41 @@ func (w *webfs) Build() HandlerFunc {
fsHandler.ServeHTTP(writer, ctx.Request)
}
if len(w.exceptions) > 0 {
middleware := make(Middleware, len(w.exceptions)+1)
for i := range w.exceptions {
middleware[i] = Prioritize(w.exceptions[i])
}
middleware[len(w.exceptions)] = HandlerFunc(h)
w.handler = func(ctx *Context) {
ctx.Middleware = append(middleware, ctx.Middleware...)
ctx.Do()
}
} else {
w.handler = h
}
})
return w.handler
}
// StripPrefix returns a handler that serves HTTP requests
// by removing the given prefix from the request URL's Path
// and invoking the handler h. StripPrefix handles a
// request for a path that doesn't begin with prefix by
// replying with an HTTP 404 not found error.
func StripPrefix(prefix string, h HandlerFunc) HandlerFunc {
if prefix == "" {
return h
}
return func(ctx *Context) {
if p := strings.TrimPrefix(ctx.Request.URL.Path, prefix); len(p) < len(ctx.Request.URL.Path) {
ctx.Request.URL.Path = p
h(ctx)
} else {
ctx.NotFound()
}
}
}