From c6f5406c3b3d0b9d6337a5bb0f466db78b5103b6 Mon Sep 17 00:00:00 2001 From: Gerasimos Maropoulos Date: Sun, 14 Aug 2016 05:44:36 +0300 Subject: [PATCH] Better gzip managment, align with https://github.com/kataras/iris/issues/361 . OnError on Parties: https://github.com/kataras/iris/issues/35 --- context.go | 65 ++++++++++----------- context/context.go | 2 - context_test.go | 24 -------- http.go | 35 ++++------- iris.go | 141 +++++++++++++++++++++++++++++++++++---------- response.go | 5 +- template.go | 5 +- 7 files changed, 160 insertions(+), 117 deletions(-) diff --git a/context.go b/context.go index 866ca089..f75c3f62 100644 --- a/context.go +++ b/context.go @@ -37,6 +37,12 @@ const ( contentType = "Content-Type" // ContentLength represents the header["Content-Length"] contentLength = "Content-Length" + // contentEncodingHeader represents the header["Content-Encoding"] + contentEncodingHeader = "Content-Encoding" + // varyHeader represents the header "Vary" + varyHeader = "Vary" + // acceptEncodingHeader represents the header key & value "Accept-Encoding" + acceptEncodingHeader = "Accept-Encoding" // ContentHTML is the string of text/html response headers contentHTML = "text/html" // ContentBinary header value for binary data. @@ -111,36 +117,6 @@ func (ctx *Context) GetRequestCtx() *fasthttp.RequestCtx { return ctx.RequestCtx } -// Reset resets the Context with a given domain.Response and domain.Request -// the context is ready-to-use after that, just like a new Context -// I use it for zero rellocation memory -func (ctx *Context) Reset(reqCtx *fasthttp.RequestCtx) { - ctx.Params = ctx.Params[0:0] - ctx.session = nil - ctx.middleware = nil - ctx.RequestCtx = reqCtx -} - -// Clone use that method if you want to use the context inside a goroutine -func (ctx *Context) Clone() context.IContext { - var cloneContext = *ctx - cloneContext.pos = 0 - - //copy params - p := ctx.Params - cpP := make(PathParameters, len(p)) - copy(cpP, p) - cloneContext.Params = cpP - //copy middleware - m := ctx.middleware - cpM := make(Middleware, len(m)) - copy(cpM, m) - cloneContext.middleware = cpM - - // we don't copy the sessionStore for more than one reasons... - return &cloneContext -} - // Do calls the first handler only, it's like Next with negative pos, used only on Router&MemoryRouter func (ctx *Context) Do() { ctx.pos = 0 @@ -521,11 +497,27 @@ func (ctx *Context) Write(format string, a ...interface{}) { ctx.RequestCtx.WriteString(fmt.Sprintf(format, a...)) } +func (ctx *Context) clientAllowsGzip() bool { + if h := ctx.RequestHeader(acceptEncodingHeader); h != "" { + for _, v := range strings.Split(h, ";") { + if strings.Contains(v, "gzip") { // we do Contains because sometimes browsers has the q=, we don't use it atm. || strings.Contains(v,"deflate"){ + return true + } + } + } + + return false +} + // Gzip accepts bytes, which are compressed to gzip format and sent to the client func (ctx *Context) Gzip(b []byte, status int) { - _, err := fasthttp.WriteGzip(ctx.RequestCtx.Response.BodyWriter(), b) - if err == nil { - ctx.RequestCtx.Response.Header.Add("Content-Encoding", "gzip") + ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader) + + if ctx.clientAllowsGzip() { + _, err := fasthttp.WriteGzip(ctx.RequestCtx.Response.BodyWriter(), b) + if err == nil { + ctx.SetHeader(contentEncodingHeader, "gzip") + } } } @@ -630,8 +622,9 @@ func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime ctx.RequestCtx.Response.Header.Set(lastModified, modtime.UTC().Format(config.TimeFormat)) ctx.RequestCtx.SetStatusCode(StatusOK) var out io.Writer - if gzipCompression { - ctx.RequestCtx.Response.Header.Add("Content-Encoding", "gzip") + if gzipCompression && ctx.clientAllowsGzip() { + ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader) + ctx.SetHeader(contentEncodingHeader, "gzip") gzipWriter := gzipWriterPool.Get().(*gzip.Writer) gzipWriter.Reset(ctx.RequestCtx.Response.BodyWriter()) defer gzipWriter.Close() @@ -651,6 +644,7 @@ func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime // gzipCompression (bool) // // You can define your own "Content-Type" header also, after this function call +// This function doesn't implement resuming, use ctx.RequestCtx.SendFile/fasthttp.ServeFileUncompressed(ctx.RequestCtx,path)/ServeFile(ctx.RequestCtx,path) instead func (ctx *Context) ServeFile(filename string, gzipCompression bool) error { f, err := os.Open(filename) if err != nil { @@ -673,6 +667,7 @@ func (ctx *Context) ServeFile(filename string, gzipCompression bool) error { // // You can define your own "Content-Type" header also, after this function call // for example: ctx.Response.Header.Set("Content-Type","thecontent/type") +// This function doesn't implement resuming, use ctx.RequestCtx.SendFile instead func (ctx *Context) SendFile(filename string, destinationName string) error { err := ctx.ServeFile(filename, false) if err != nil { diff --git a/context/context.go b/context/context.go index f02864f1..2a43ef68 100644 --- a/context/context.go +++ b/context/context.go @@ -90,9 +90,7 @@ type ( Session() Session SessionDestroy() Log(string, ...interface{}) - Reset(*fasthttp.RequestCtx) GetRequestCtx() *fasthttp.RequestCtx - Clone() IContext Do() Next() StopExecution() diff --git a/context_test.go b/context_test.go index ea7c3bb1..8e59412c 100644 --- a/context_test.go +++ b/context_test.go @@ -21,30 +21,6 @@ import ( "github.com/valyala/fasthttp" ) -func TestContextReset(t *testing.T) { - var context Context - context.Params = PathParameters{PathParameter{Key: "testkey", Value: "testvalue"}} - context.Reset(nil) - if len(context.Params) > 0 { - t.Fatalf("Expecting to have %d params but got: %d", 0, len(context.Params)) - } -} - -func TestContextClone(t *testing.T) { - var context Context - context.Params = PathParameters{ - PathParameter{Key: "testkey", Value: "testvalue"}, - PathParameter{Key: "testkey2", Value: "testvalue2"}, - } - c := context.Clone() - if v := c.Param("testkey"); v != context.Param("testkey") { - t.Fatalf("Expecting to have parameter value: %s but got: %s", context.Param("testkey"), v) - } - if v := c.Param("testkey2"); v != context.Param("testkey2") { - t.Fatalf("Expecting to have parameter value: %s but got: %s", context.Param("testkey2"), v) - } -} - func TestContextDoNextStop(t *testing.T) { var context Context ok := false diff --git a/http.go b/http.go index f6c7e5b4..14eb75d9 100644 --- a/http.go +++ b/http.go @@ -311,9 +311,8 @@ func (s *Server) Port() int { return 443 } return 80 - } else { - return p } + return p } if s.Config.AutoTLS { return 443 @@ -559,12 +558,10 @@ func (s *ServerList) CloseAll() (err error) { // OpenAll starts all servers // returns the first error happens to one of these servers // if one server gets error it closes the previous servers and exits from this process -func (s *ServerList) OpenAll() error { +func (s *ServerList) OpenAll(reqHandler fasthttp.RequestHandler) error { l := len(s.servers) - 1 - h := s.mux.ServeRequest() for i := range s.servers { - - if err := s.servers[i].Open(h); err != nil { + if err := s.servers[i].Open(reqHandler); err != nil { time.Sleep(2 * time.Second) // for any case, // we don't care about performance on initialization, @@ -1332,7 +1329,6 @@ type ( } serveMux struct { - cPool *sync.Pool tree *muxTree lookups []*route @@ -1355,9 +1351,8 @@ type ( } ) -func newServeMux(contextPool sync.Pool, logger *logger.Logger) *serveMux { +func newServeMux(logger *logger.Logger) *serveMux { mux := &serveMux{ - cPool: &contextPool, lookups: make([]*route, 0), errorHandlers: make(map[int]Handler, 0), hostname: config.DefaultServerHostname, // these are changing when the server is up @@ -1485,16 +1480,16 @@ func (mux *serveMux) lookup(routeName string) *route { return nil } -func (mux *serveMux) ServeRequest() fasthttp.RequestHandler { +func (mux *serveMux) Handler() HandlerFunc { // initialize the router once mux.build() // optimize this once once, we could do that: context.RequestPath(mux.escapePath), but we lose some nanoseconds on if :) - getRequestPath := func(reqCtx *fasthttp.RequestCtx) string { - return utils.BytesToString(reqCtx.Path()) + getRequestPath := func(ctx *Context) string { + return utils.BytesToString(ctx.Path()) //string(ctx.Path()[:]) // a little bit of memory allocation, old method used: BytesToString, If I see the benchmarks get low I will change it back to old, but this way is safer. } if !mux.escapePath { - getRequestPath = func(reqCtx *fasthttp.RequestCtx) string { return utils.BytesToString(reqCtx.RequestURI()) } + getRequestPath = func(ctx *Context) string { return utils.BytesToString(ctx.RequestCtx.RequestURI()) } } methodEqual := func(treeMethod []byte, reqMethod []byte) bool { @@ -1511,14 +1506,11 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler { } } - return func(reqCtx *fasthttp.RequestCtx) { - context := mux.cPool.Get().(*Context) - context.Reset(reqCtx) - - routePath := getRequestPath(reqCtx) + return func(context *Context) { + routePath := getRequestPath(context) tree := mux.tree for tree != nil { - if !methodEqual(tree.method, reqCtx.Method()) { + if !methodEqual(tree.method, context.Method()) { // we break any CORS OPTIONS method // but for performance reasons if user wants http method OPTIONS to be served // then must register it with .Options(...) @@ -1555,9 +1547,8 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler { context.middleware = middleware //ctx.Request.Header.SetUserAgentBytes(DefaultUserAgent) context.Do() - mux.cPool.Put(context) return - } else if mustRedirect && mux.correctPath && !bytes.Equal(reqCtx.Method(), methodConnectBytes) { + } else if mustRedirect && mux.correctPath && !bytes.Equal(context.Method(), methodConnectBytes) { reqPath := routePath pathLen := len(reqPath) @@ -1582,7 +1573,6 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler { note := "Moved Permanently.\n" context.Write(note) } - mux.cPool.Put(context) return } } @@ -1590,6 +1580,5 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler { break } mux.fireError(StatusNotFound, context) - mux.cPool.Put(context) } } diff --git a/iris.go b/iris.go index 61a75bf7..eb6bc720 100644 --- a/iris.go +++ b/iris.go @@ -153,8 +153,6 @@ type ( UseTemplate(TemplateEngine) *TemplateEngineLocation UseGlobal(...Handler) UseGlobalFunc(...HandlerFunc) - OnError(int, HandlerFunc) - EmitError(int, *Context) Lookup(string) Route Lookups() []Route Path(string, ...interface{}) string @@ -169,6 +167,7 @@ type ( // Implements the FrameworkAPI Framework struct { *muxAPI + contextPool sync.Pool Config *config.Iris gzipWriterPool sync.Pool // used for several methods, usually inside context sessions *sessionsManager @@ -225,7 +224,7 @@ func New(cfg ...config.Iris) *Framework { // set the websocket server s.Websocket = NewWebsocketServer(s.Config.Websocket) // set the servemux, which will provide us the public API also, with its context pool - mux := newServeMux(sync.Pool{New: func() interface{} { return &Context{framework: s} }}, s.Logger) + mux := newServeMux(s.Logger) mux.onLookup = s.Plugins.DoPreLookup // set the public router API (and party) s.muxAPI = &muxAPI{mux: mux, relativePath: "/"} @@ -286,6 +285,29 @@ func (s *Framework) initialize() { } } +func (s *Framework) acquireCtx(reqCtx *fasthttp.RequestCtx) *Context { + v := s.contextPool.Get() + var ctx *Context + if v == nil { + ctx = &Context{ + RequestCtx: reqCtx, + framework: s, + } + } else { + ctx = v.(*Context) + ctx.Params = ctx.Params[0:0] + ctx.RequestCtx = reqCtx + ctx.middleware = nil + ctx.session = nil + } + + return ctx +} + +func (s *Framework) releaseCtx(ctx *Context) { + s.contextPool.Put(ctx) +} + // Go starts the iris station, listens to all registered servers, and prepare only if Virtual func Go() error { return Default.Go() @@ -295,8 +317,14 @@ func Go() error { func (s *Framework) Go() error { s.initialize() s.Plugins.DoPreListen(s) - - if firstErr := s.Servers.OpenAll(); firstErr != nil { + // build the fasthttp handler to bind it to the servers + h := s.mux.Handler() + reqHandler := func(reqCtx *fasthttp.RequestCtx) { + ctx := s.acquireCtx(reqCtx) + h(ctx) + s.releaseCtx(ctx) + } + if firstErr := s.Servers.OpenAll(reqHandler); firstErr != nil { return firstErr } @@ -665,30 +693,6 @@ func (s *Framework) UseGlobalFunc(handlersFn ...HandlerFunc) { s.UseGlobal(convertToHandlers(handlersFn)...) } -// OnError registers a custom http error handler -func OnError(statusCode int, handlerFn HandlerFunc) { - Default.OnError(statusCode, handlerFn) -} - -// EmitError fires a custom http error handler to the client -// -// if no custom error defined with this statuscode, then iris creates one, and once at runtime -func EmitError(statusCode int, ctx *Context) { - Default.EmitError(statusCode, ctx) -} - -// OnError registers a custom http error handler -func (s *Framework) OnError(statusCode int, handlerFn HandlerFunc) { - s.mux.registerError(statusCode, handlerFn) -} - -// EmitError fires a custom http error handler to the client -// -// if no custom error defined with this statuscode, then iris creates one, and once at runtime -func (s *Framework) EmitError(statusCode int, ctx *Context) { - s.mux.fireError(statusCode, ctx) -} - // Lookup returns a registed route by its name func Lookup(routeName string) Route { return Default.Lookup(routeName) @@ -1051,6 +1055,10 @@ type ( // templates Layout(string) MuxAPI // returns itself + + // errors + OnError(int, HandlerFunc) + EmitError(int, *Context) } muxAPI struct { @@ -1839,3 +1847,78 @@ func (api *muxAPI) Layout(tmplLayoutFile string) MuxAPI { }) return api } + +// OnError registers a custom http error handler +func OnError(statusCode int, handlerFn HandlerFunc) { + Default.OnError(statusCode, handlerFn) +} + +// EmitError fires a custom http error handler to the client +// +// if no custom error defined with this statuscode, then iris creates one, and once at runtime +func EmitError(statusCode int, ctx *Context) { + Default.EmitError(statusCode, ctx) +} + +// OnError registers a custom http error handler +func (api *muxAPI) OnError(statusCode int, handlerFn HandlerFunc) { + + path := strings.Replace(api.relativePath, "//", "/", -1) // fix the path if double // + staticPath := path + // find the static path (on Party the path should be ALWAYS a static path, as we all know, + // but do this check for any case) + dynamicPathIdx := strings.IndexByte(path, parameterStartByte) // check for /mypath/:param + + if dynamicPathIdx == -1 { + dynamicPathIdx = strings.IndexByte(path, matchEverythingByte) // check for /mypath/*param + } + + if dynamicPathIdx > 1 { //yes after / and one character more ( /*param or /:param will break the root path, and this is not allowed even on error handlers). + staticPath = api.relativePath[0:dynamicPathIdx] + } + + if staticPath == "/" { + api.mux.registerError(statusCode, handlerFn) // register the user-specific error message, as the global error handler, for now. + return + } + + //after this, we have more than one error handler for one status code, and that's dangerous some times, but use it for non-globals error catching by your own risk + // NOTES: + // subdomains error will not work if same path of a non-subdomain (maybe a TODO for later) + // errors for parties should be registered from the biggest path length to the smaller. + + // get the previous + prevErrHandler := api.mux.errorHandlers[statusCode] + if prevErrHandler == nil { + /* + make a new one with the standard error message, + this will be used as the last handler if no other error handler catches the error (by prefix(?)) + */ + prevErrHandler = HandlerFunc(func(ctx *Context) { + ctx.ResetBody() + ctx.SetStatusCode(statusCode) + ctx.SetBodyString(statusText[statusCode]) + }) + } + + func(statusCode int, staticPath string, prevErrHandler Handler, newHandler Handler) { // to separate the logic + errHandler := HandlerFunc(func(ctx *Context) { + if strings.HasPrefix(ctx.PathString(), staticPath) { // yes the user should use OnError from longest to lower static path's length in order this to work, so we can find another way, like a builder on the end. + newHandler.Serve(ctx) + return + } + // serve with the user-specific global ("/") pure iris.OnError receiver Handler or the standar handler if OnError called only from inside a no-relative Party. + prevErrHandler.Serve(ctx) + }) + + api.mux.registerError(statusCode, errHandler) + }(statusCode, staticPath, prevErrHandler, handlerFn) + +} + +// EmitError fires a custom http error handler to the client +// +// if no custom error defined with this statuscode, then iris creates one, and once at runtime +func (api *muxAPI) EmitError(statusCode int, ctx *Context) { + api.mux.fireError(statusCode, ctx) +} diff --git a/response.go b/response.go index ee36ebe3..72e30f70 100644 --- a/response.go +++ b/response.go @@ -151,12 +151,13 @@ func (r *responseEngineMap) render(ctx *Context, obj interface{}, options ...map } ctx.SetContentType(ctype) - if gzipEnabled { + if gzipEnabled && ctx.clientAllowsGzip() { _, err := fasthttp.WriteGzip(ctx.RequestCtx.Response.BodyWriter(), finalResult) if err != nil { return err } - ctx.Response.Header.Add("Content-Encoding", "gzip") + ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader) + ctx.SetHeader(contentEncodingHeader, "gzip") } else { ctx.Response.SetBody(finalResult) } diff --git a/template.go b/template.go index 81514521..25d4723e 100644 --- a/template.go +++ b/template.go @@ -201,8 +201,9 @@ func (t *templateEngineWrapper) execute(ctx *Context, filename string, binding i ctx.SetContentType(contentHTML + "; charset=" + charset) var out io.Writer - if gzipEnabled { - ctx.Response.Header.Add("Content-Encoding", "gzip") + if gzipEnabled && ctx.clientAllowsGzip() { + ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader) + ctx.SetHeader(contentEncodingHeader, "gzip") gzipWriter := ctx.framework.AcquireGzip(ctx.Response.BodyWriter()) defer ctx.framework.ReleaseGzip(gzipWriter)