diff --git a/HISTORY.md b/HISTORY.md index 12a13b12..e11170ed 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,6 +17,13 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you. +# Th, 09 November 2017 | v8.5.8 + +- **IMPROVE** the `Single Page Application builder`[*](https://github.com/kataras/blob/master/core/router/router_spa_wrapper.go) and fix https://github.com/kataras/iris/issues/803 reported by @ionutvilie, a new example is located [here](_examples/file-server/embedded-single-page-application-with-other-routes). + * `app.SPA` now returns the `*SPABuilder` and you can change some of its fields manually, i.e; + * `IndexNames` defaulted to empty but can be seted to `[]{"index.html"}` or call the **new** `AddIndexName` from the `app.SPA` manually if dynamic view on root has registered, see [here](_examples/file-server/single-page-application/embedded-single-page-application) how. + * `AssetValidator` exists as it was and it's checked before the spa file server but it allows everything by-default because the real validation happens internally; if body was written or not, if not then reset the context's response writer and execute the router, as previously, otherwise release the context and send the response to the client. + # Tu, 07 November 2017 | v8.5.7 Nothing crazy here, just one addition which may help some people; diff --git a/README.md b/README.md index 3d049666..27e3c1ad 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If you're coming from [nodejs](https://nodejs.org) world, Iris is the [expressjs ## Table Of Content * [Installation](#installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-07-november-2017--v857) +* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#th-09-november-2017--v858) * [Getting started](#getting-started) * [Learn](_examples/) * [MVC (Model View Controller)](_examples/#mvc) **NEW** @@ -145,11 +145,7 @@ func main() { ``` -```sh -$ go run main.go -> Now listening on: http://localhost:8080 -> Application started. Press CTRL+C to shut down. -``` +![overview screen](https://github.com/kataras/build-a-better-web-together/raw/master/overview_screen_1.png) > Wanna re-start your app automatically when source code changes happens? Install the [rizla](https://github.com/kataras/rizla) tool and run `rizla main.go` instead of `go run main.go`. diff --git a/VERSION b/VERSION index 60a5aaea..87e1dd41 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.5.7:https://github.com/kataras/iris/blob/master/HISTORY.md#tu-07-november-2017--v857 \ No newline at end of file +8.5.8:https://github.com/kataras/iris/blob/master/HISTORY.md#th-09-november-2017--v858 \ No newline at end of file diff --git a/_examples/README.md b/_examples/README.md index 4a23bed3..77b764b1 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -17,6 +17,7 @@ It doesn't always contain the "best ways" but it does cover each important featu - [POC: Isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/kataras/iris-starter-kit) - [Tutorial: DropzoneJS Uploader](tutorial/dropzonejs) - [Tutorial: Caddy](tutorial/caddy) +- [Tutorial:Iris Go Framework + MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) ### Structuring @@ -276,6 +277,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files to - Single Page Applications * [single Page Application](file-server/single-page-application/basic/main.go) * [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) + * [embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) ### How to Read from `context.Request() *http.Request` diff --git a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/bindata.go b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/bindata.go new file mode 100644 index 00000000..74181a48 --- /dev/null +++ b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/bindata.go @@ -0,0 +1,285 @@ +// Code generated by go-bindata. +// sources: +// public/app.js +// public/css/main.css +// public/index.html +// DO NOT EDIT! + +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _publicAppJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2a\xcf\xcc\x4b\xc9\x2f\xd7\x4b\xcc\x49\x2d\x2a\xd1\x50\x4a\x2c\x28\xd0\xcb\x2a\x56\xc8\xc9\x4f\x4c\x49\x4d\x51\x48\x2b\xca\xcf\x55\x88\x51\xd2\x57\xd2\xb4\x06\x04\x00\x00\xff\xff\xa9\x06\xf7\xa3\x27\x00\x00\x00") + +func publicAppJsBytes() ([]byte, error) { + return bindataRead( + _publicAppJs, + "public/app.js", + ) +} + +func publicAppJs() (*asset, error) { + bytes, err := publicAppJsBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "public/app.js", size: 39, mode: os.FileMode(511), modTime: time.Unix(1499700236, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _publicCssMainCss = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4a\xca\x4f\xa9\x54\xa8\xe6\xe5\x52\x50\x50\x50\x48\x4a\x4c\xce\x4e\x2f\xca\x2f\xcd\x4b\xd1\x4d\xce\xcf\xc9\x2f\xb2\x52\x48\xca\x49\x4c\xce\xb6\xe6\xe5\xaa\xe5\xe5\x02\x04\x00\x00\xff\xff\x03\x25\x9c\x89\x29\x00\x00\x00") + +func publicCssMainCssBytes() ([]byte, error) { + return bindataRead( + _publicCssMainCss, + "public/css/main.css", + ) +} + +func publicCssMainCss() (*asset, error) { + bytes, err := publicCssMainCssBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "public/css/main.css", size: 41, mode: os.FileMode(511), modTime: time.Unix(1499700236, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _publicIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\x8e\x41\x0e\xc2\x20\x10\x45\xf7\x24\xdc\xe1\xa7\x07\x28\xe9\x7e\x64\xed\x35\x10\x46\xc1\x50\x21\x30\x0b\xbd\xbd\x29\xc5\xc4\xf5\x7f\x6f\xde\x50\x94\x3d\x5b\xad\xb4\xa2\xc8\x2e\x58\xad\x00\x80\x24\x49\x66\x7b\xe5\x9c\x0b\xee\xad\xec\xe8\xe2\x24\x79\x54\xf7\x60\x32\xe7\xaa\x15\x99\xe9\x68\x45\xb7\x12\x3e\x3f\x3b\x6e\x16\x7f\x6e\x7a\x05\x7e\xaf\x47\x08\x64\xe2\x36\xf8\x49\x76\xdf\x52\x15\xf4\xe6\x2f\x8b\x71\xb5\xae\xcf\xbe\x58\x80\xcc\x39\x8c\xc6\xbc\x3c\x72\xc7\xb3\xdf\x00\x00\x00\xff\xff\x7e\xad\xd1\x97\xb3\x00\x00\x00") + +func publicIndexHtmlBytes() ([]byte, error) { + return bindataRead( + _publicIndexHtml, + "public/index.html", + ) +} + +func publicIndexHtml() (*asset, error) { + bytes, err := publicIndexHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "public/index.html", size: 179, mode: os.FileMode(511), modTime: time.Unix(1510219864, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "public/app.js": publicAppJs, + "public/css/main.css": publicCssMainCss, + "public/index.html": publicIndexHtml, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "public": &bintree{nil, map[string]*bintree{ + "app.js": &bintree{publicAppJs, map[string]*bintree{}}, + "css": &bintree{nil, map[string]*bintree{ + "main.css": &bintree{publicCssMainCss, map[string]*bintree{}}, + }}, + "index.html": &bintree{publicIndexHtml, map[string]*bintree{}}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go new file mode 100644 index 00000000..81d7c707 --- /dev/null +++ b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go @@ -0,0 +1,56 @@ +package main + +import "github.com/kataras/iris" + +// $ go get -u github.com/jteeuwen/go-bindata/... +// $ go-bindata ./public/... +// $ go build +// $ ./embedded-single-page-application-with-other-routes + +func newApp() *iris.Application { + app := iris.New() + app.OnErrorCode(404, func(ctx iris.Context) { + ctx.Writef("404 not found here") + }) + + assetHandler := app.StaticEmbeddedHandler("./public", Asset, AssetNames) + app.SPA(assetHandler) + + // Note: + // if you want a dynamic index page then see the file-server/embedded-single-page-application + // which is registering a view engine based on bindata as well and a root route. + + app.Get("/ping", func(ctx iris.Context) { + ctx.WriteString("pong") + }) + app.Get("/.well-known", func(ctx iris.Context) { + ctx.WriteString("well-known") + }) + app.Get(".well-known/ready", func(ctx iris.Context) { + ctx.WriteString("ready") + }) + app.Get(".well-known/live", func(ctx iris.Context) { + ctx.WriteString("live") + }) + app.Get(".well-known/metrics", func(ctx iris.Context) { + ctx.Writef("metrics") + }) + return app +} + +func main() { + app := newApp() + + // http://localhost:8080/index.html + // http://localhost:8080/app.js + // http://localhost:8080/css/main.css + // + // http://localhost:8080/ping + // http://localhost:8080/.well-known + // http://localhost:8080/.well-known/ready + // http://localhost:8080/.well-known/live + // http://localhost:8080/.well-known/metrics + // + // Remember: we could use the root wildcard `app.Get("/{param:path}")` and serve the files manually as well. + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/app.js b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/app.js new file mode 100644 index 00000000..c47a1fd5 --- /dev/null +++ b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/app.js @@ -0,0 +1 @@ +window.alert("app.js loaded from \"/"); \ No newline at end of file diff --git a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/css/main.css b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/css/main.css new file mode 100644 index 00000000..7db3df1d --- /dev/null +++ b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/css/main.css @@ -0,0 +1,3 @@ +body { + background-color: black; +} diff --git a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/index.html b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/index.html new file mode 100644 index 00000000..e36f1bfe --- /dev/null +++ b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/public/index.html @@ -0,0 +1,14 @@ + + + + Hello from static page + + + +

Hello from index.html

+ + + + + + \ No newline at end of file diff --git a/_examples/file-server/single-page-application/embedded-single-page-application/main.go b/_examples/file-server/single-page-application/embedded-single-page-application/main.go index 1f8d28e4..ec3d338b 100644 --- a/_examples/file-server/single-page-application/embedded-single-page-application/main.go +++ b/_examples/file-server/single-page-application/embedded-single-page-application/main.go @@ -25,7 +25,11 @@ func newApp() *iris.Application { assetHandler := app.StaticEmbeddedHandler("./public", Asset, AssetNames) // as an alternative of SPA you can take a look at the /routing/dynamic-path/root-wildcard // example too - app.SPA(assetHandler) + + // public/index.html is a dynamic view, it's handlded by root, + // and we don't want to be visible as a raw data, so we will + // the return value of `app.SPA` to modify the `IndexNames` by; + app.SPA(assetHandler).AddIndexName("index.html") return app } diff --git a/context/pool.go b/context/pool.go index 30ab5523..49f4738b 100644 --- a/context/pool.go +++ b/context/pool.go @@ -44,3 +44,10 @@ func (c *Pool) Release(ctx Context) { ctx.EndRequest() c.pool.Put(ctx) } + +// ReleaseLight will just release the object back to the pool, but the +// clean method is caller's responsibility now, currently this is only used +// on `SPABuilder`. +func (c *Pool) ReleaseLight(ctx Context) { + c.pool.Put(ctx) +} diff --git a/core/maintenance/version.go b/core/maintenance/version.go index a5e019c8..2523b3d8 100644 --- a/core/maintenance/version.go +++ b/core/maintenance/version.go @@ -13,7 +13,7 @@ import ( const ( // Version is the string representation of the current local Iris Web Framework version. - Version = "8.5.7" + Version = "8.5.8" ) // CheckForUpdates checks for any available updates diff --git a/core/router/fs.go b/core/router/fs.go index 8885f4d5..3ab54947 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -21,6 +21,49 @@ import ( "github.com/kataras/iris/context" ) +func calculateAssetValidator(vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) AssetValidator { + if len(vdir) > 0 { + if vdir[0] == '.' { + vdir = vdir[1:] + } + if vdir[0] == '/' || vdir[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed + vdir = vdir[1:] + } + } + + // collect the names we are care for, + // because not all Asset used here, we need the vdir's assets. + allNames := namesFn() + + var names []string + for _, path := range allNames { + // i.e: path = public/css/main.css + + // check if path is the path name we care for + if !strings.HasPrefix(path, vdir) { + continue + } + + names = append(names, path) + } + + return func(reqPath string) bool { + reqPath = strings.TrimPrefix(reqPath, "/"+vdir) + + for _, path := range names { + // in order to map "/" as "/index.html" + if path == "/index.html" && reqPath == "/" { + reqPath = "/index.html" + } + + if path == vdir+reqPath { + return true + } + } + return false + } +} + // StaticEmbeddedHandler returns a Handler which can serve // embedded into executable files. // @@ -766,7 +809,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo // can't use Redirect() because that would make the path absolute, // which would be a problem running under StripPrefix if strings.HasSuffix(ctx.Request().URL.Path, indexPage) { - localRedirect(ctx, "./") + localRedirect(ctx.ResponseWriter(), ctx.Request(), "./") return "", http.StatusMovedPermanently } @@ -787,12 +830,12 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo url := ctx.Request().URL.Path if d.IsDir() { if url[len(url)-1] != '/' { - localRedirect(ctx, path.Base(url)+"/") + localRedirect(ctx.ResponseWriter(), ctx.Request(), path.Base(url)+"/") return "", http.StatusMovedPermanently } } else { if url[len(url)-1] == '/' { - localRedirect(ctx, "../"+path.Base(url)) + localRedirect(ctx.ResponseWriter(), ctx.Request(), "../"+path.Base(url)) return "", http.StatusMovedPermanently } } @@ -802,7 +845,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo if d.IsDir() { url := ctx.Request().URL.Path if url[len(url)-1] != '/' { - localRedirect(ctx, path.Base(url)+"/") + localRedirect(ctx.ResponseWriter(), ctx.Request(), path.Base(url)+"/") return "", http.StatusMovedPermanently } } @@ -885,12 +928,14 @@ func toHTTPError(err error) (msg string, httpStatus int) { // localRedirect gives a Moved Permanently response. // It does not convert relative paths to absolute paths like Redirect does. -func localRedirect(ctx context.Context, newPath string) { - if q := ctx.Request().URL.RawQuery; q != "" { +func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) { + if q := r.URL.RawQuery; q != "" { newPath += "?" + q } - ctx.Header("Location", newPath) - ctx.StatusCode(http.StatusMovedPermanently) + w.Header().Set("Location", newPath) + w.WriteHeader(http.StatusMovedPermanently) + // ctx.Header("Location", newPath) + // ctx.StatusCode(http.StatusMovedPermanently) } func containsDotDot(v string) bool { diff --git a/core/router/router_spa_wrapper.go b/core/router/router_spa_wrapper.go index 6b6f0a13..a43387bb 100644 --- a/core/router/router_spa_wrapper.go +++ b/core/router/router_spa_wrapper.go @@ -19,6 +19,15 @@ type SPABuilder struct { AssetValidators []AssetValidator } +// AddIndexName will add an index name. +// If path == $filename then it redirects to "/". +// +// It can be called after the `BuildWrapper ` as well but BEFORE the server start. +func (s *SPABuilder) AddIndexName(filename string) *SPABuilder { + s.IndexNames = append(s.IndexNames, filename) + return s +} + // NewSPABuilder returns a new Single Page Application builder // It does what StaticWeb expected to do when serving files and routes at the same time // from the root "/" path. @@ -32,11 +41,13 @@ func NewSPABuilder(assetHandler context.Handler) *SPABuilder { } return &SPABuilder{ - IndexNames: []string{"index.html"}, + IndexNames: nil, + // IndexNames is empty by-default, + // if the user wants to redirect to "/" from "/index.html" she/he can chage that to []string{"index.html"} manually. AssetHandler: assetHandler, AssetValidators: []AssetValidator{ func(path string) bool { - return strings.Contains(path, ".") + return true // returns true by-default }, }, } @@ -61,23 +72,23 @@ func (s *SPABuilder) isAsset(reqPath string) bool { func (s *SPABuilder) BuildWrapper(cPool *context.Pool) WrapperFunc { fileServer := s.AssetHandler - indexNames := s.IndexNames wrapper := func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { path := r.URL.Path + // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything + // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. + // + // It's here for backwards compatibility as well, see #803. if !s.isAsset(path) { // it's not asset, execute the registered route's handlers router(w, r) return } - ctx := cPool.Acquire(w, r) - - for _, index := range indexNames { + for _, index := range s.IndexNames { if strings.HasSuffix(path, index) { - localRedirect(ctx, "./") - cPool.Release(ctx) + localRedirect(w, r, "./") // "/" should be manually registered. // We don't setup an index handler here, // let full control to the user @@ -86,9 +97,28 @@ func (s *SPABuilder) BuildWrapper(cPool *context.Pool) WrapperFunc { } } - // execute file server for path + ctx := cPool.Acquire(w, r) + // convert to a recorder in order to not write the status and body directly but wait for a flush (EndRequest). + rec := ctx.Recorder() // rec and context.ResponseWriter() is the same thing now. + // execute the asset handler. fileServer(ctx) - + // check if body was written, if not then; + // 1. reset the whole response writer, its status code, headers and body + // 2. release only the object, + // so it doesn't fires the status code's handler to the client + // (we are eliminating the multiple response header calls this way) + // 3. execute the router itself, if route found then it will serve that, otherwise 404 or 405. + // + // we could also use the ctx.ResponseWriter().Written() > 0. + empty := len(rec.Body()) == 0 + if empty { + rec.Reset() + cPool.ReleaseLight(ctx) + router(w, r) + return + } + // if body was written from the file server then release the context as usual, + // it will send everything to the client and reset the context. cPool.Release(ctx) } diff --git a/doc.go b/doc.go index c00e752b..3eaed501 100644 --- a/doc.go +++ b/doc.go @@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub: Current Version -8.5.7 +8.5.8 Installation diff --git a/iris.go b/iris.go index 29853228..10651ab7 100644 --- a/iris.go +++ b/iris.go @@ -343,10 +343,11 @@ var ( // Use that instead of `StaticWeb` for root "/" file server. // // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application -func (app *Application) SPA(assetHandler context.Handler) { +func (app *Application) SPA(assetHandler context.Handler) *router.SPABuilder { s := router.NewSPABuilder(assetHandler) wrapper := s.BuildWrapper(app.ContextPool) app.Router.WrapRouter(wrapper) + return s } // ConfigureHost accepts one or more `host#Configuration`, these configurators functions