From ef7d365e8186394c4bc7af6565ffadc7d53006b1 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 15 Aug 2020 17:21:57 +0300 Subject: [PATCH] add iris.Minify middleware and Context.OnCloseErr/OnConnectionCloseErr --- HISTORY.md | 4 ++ NOTICE | 7 ++- _examples/i18n/main.go | 2 +- .../subdomains/http-errors-view/main.go | 7 +++ .../subdomains/http-errors-view/main_test.go | 40 ++++++++++++ aliases.go | 17 ++++++ context/application.go | 9 +++ context/context.go | 61 ++++++++++++++++++- go.mod | 1 + iris.go | 34 +++++++++++ 10 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 _examples/routing/subdomains/http-errors-view/main_test.go diff --git a/HISTORY.md b/HISTORY.md index fe52f7b6..e1fb2fbc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -395,6 +395,10 @@ func main() { } ``` +- `iris.Minify` middleware to minify responses based on their media/content-type. + +- `Context.OnCloseErr` and `Context.OnConnectionCloseErr` - to call a function of `func() error` instead of an `iris.Handler` when request is closed or manually canceled. + - `Party.UseError(...Handler)` - to register handlers to run before the `OnErrorCode/OnAnyErrorCode` ones. - `Party.UseRouter(...Handler)` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS. diff --git a/NOTICE b/NOTICE index 53742667..716ea617 100644 --- a/NOTICE +++ b/NOTICE @@ -6,7 +6,7 @@ The following 3rd-party software packages may be used by or distributed with iris. This document was automatically generated by FOSSA on 2020-5-8; any information relevant to third-party vendors listed below are collected using common, reasonable means. -Revision ID: 46a3a99adf457d30ea4aeb13ada45e895130d718 +Revision ID: ab226d925aa394ccecf01e515ea8479367e0961c ----------------- ----------------- ------------------------------------------ Package Version Website @@ -67,7 +67,10 @@ Revision ID: 46a3a99adf457d30ea4aeb13ada45e895130d718 1029f1d962 msgpack 911bfe50493ebbc https://github.com/vmihailenco/msgpack b6e0af9e6f36451 - 255746ff46 + 255746ff46 + minify 119ab8b676c60a6 https://github.com/tdewolff/minify + 12b9cc824e6a84a + 865191aabb neffos f1431864185db0b https://github.com/kataras/neffos 334b82e3817eb03 bd957f9fda diff --git a/_examples/i18n/main.go b/_examples/i18n/main.go index e0a859b4..b084c3a6 100644 --- a/_examples/i18n/main.go +++ b/_examples/i18n/main.go @@ -14,6 +14,7 @@ func newApp() *iris.Application { if err != nil { panic(err) } + app.I18n.Subdomain // app.I18n.LoadAssets for go-bindata. // Default values: @@ -29,7 +30,6 @@ func newApp() *iris.Application { app.Get("/", func(ctx iris.Context) { hi := ctx.Tr("hi", "iris") - locale := ctx.GetLocale() ctx.Writef("From the language %s translated output: %s", locale.Language(), hi) diff --git a/_examples/routing/subdomains/http-errors-view/main.go b/_examples/routing/subdomains/http-errors-view/main.go index 0fe25702..a7a04a3a 100644 --- a/_examples/routing/subdomains/http-errors-view/main.go +++ b/_examples/routing/subdomains/http-errors-view/main.go @@ -9,11 +9,18 @@ func main() { func newApp() *iris.Application { app := iris.New() + // Create the "test.mydomain.com" subdomain. test := app.Subdomain("test") + // Register views for the test subdomain. test.RegisterView(iris.HTML("./views", ".html"). Layout("layouts/test.layout.html")) + // Optionally, to minify the HTML5 error response. + // Note that minification might be slower, caching is advised. + test.UseError(iris.Minify) + // Register error code 404 handler. test.OnErrorCode(iris.StatusNotFound, handleNotFoundTestSubdomain) + test.Get("/", testIndex) return app diff --git a/_examples/routing/subdomains/http-errors-view/main_test.go b/_examples/routing/subdomains/http-errors-view/main_test.go new file mode 100644 index 00000000..7af2fa03 --- /dev/null +++ b/_examples/routing/subdomains/http-errors-view/main_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/v12/httptest" +) + +func TestSubdomainsHTTPErrorsView(t *testing.T) { + app := newApp() + // hard coded. + expectedHTMLResponse := ` + + Test Subdomain + + + + +
+

Oups, you've got an error!

+ + +
+

Not Found

+
+ + +
+ + + ` + + e := httptest.New(t, app) + got := e.GET("/not_found").WithURL("http://test.mydomain.com").Expect().Status(httptest.StatusNotFound). + ContentType("text/html", "utf-8").Body().Raw() + + if expected, _ := app.Minifier().String("text/html", expectedHTMLResponse); expected != got { + t.Fatalf("expected:\n'%s'\nbut got:\n'%s'", expected, got) + } +} diff --git a/aliases.go b/aliases.go index 77270d9b..b609b024 100644 --- a/aliases.go +++ b/aliases.go @@ -244,6 +244,23 @@ var ( ctx.Next() } + // Minify is a middleware which minifies the responses + // based on the response content type. + // Note that minification might be slower, caching is advised. + // Customize the minifier through `Application.Minifier()`. + Minify = func(ctx Context) { + w := ctx.Application().Minifier().ResponseWriter(ctx.ResponseWriter().Naive(), ctx.Request()) + // Note(@kataras): + // We don't use defer w.Close() + // because this response writer holds a sync.WaitGroup under the hoods + // and we MUST be sure that its wg.Wait is called on request cancelation + // and not in the end of handlers chain execution + // (which if running a time-consuming task it will delay its resource release). + ctx.OnCloseErr(w.Close) + ctx.ResponseWriter().SetWriter(w) + ctx.Next() + } + // MatchImagesAssets is a simple regex expression // that can be passed to the DirOptions.Cache.CompressIgnore field // in order to skip compression on already-compressed file types diff --git a/context/application.go b/context/application.go index ab524859..b0d89e85 100644 --- a/context/application.go +++ b/context/application.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/kataras/golog" + "github.com/tdewolff/minify/v2" ) // Application is the context's owner. @@ -25,6 +26,14 @@ type Application interface { // the failure reason if not. Validate(interface{}) error + // Minifier returns the minifier instance. + // By default it can minifies: + // - text/html + // - text/css + // - image/svg+xml + // - application/text(javascript, ecmascript, json, xml). + // Use that instance to add custom Minifiers before server ran. + Minifier() *minify.M // View executes and write the result of a template file to the writer. // // Use context.View to render templates to the client instead. diff --git a/context/context.go b/context/context.go index 1619812e..d2ec69ae 100644 --- a/context/context.go +++ b/context/context.go @@ -259,6 +259,35 @@ func (ctx *Context) OnConnectionClose(cb Handler) bool { return true } +// OnConnectionCloseErr same as `OnConnectionClose` but instead it +// receives a function which returns an error. +// If error is not nil, it will be logged as a debug message. +func (ctx *Context) OnConnectionCloseErr(cb func() error) bool { + if cb == nil { + return false + } + + reqCtx := ctx.Request().Context() + if reqCtx == nil { + return false + } + + notifyClose := reqCtx.Done() + if notifyClose == nil { + return false + } + + go func() { + <-notifyClose + if err := cb(); err != nil { + // Can be ignored. + ctx.app.Logger().Debugf("OnConnectionCloseErr: received error: %v", err) + } + }() + + return true +} + // OnClose registers a callback which // will be fired when the underlying connection has gone away(request canceled) // on its own goroutine or in the end of the request-response lifecylce @@ -297,6 +326,36 @@ func (ctx *Context) OnClose(cb Handler) { ctx.writer.SetBeforeFlush(onFlush) } +// OnCloseErr same as `OnClose` but instead it +// receives a function which returns an error. +// If error is not nil, it will be logged as a debug message. +func (ctx *Context) OnCloseErr(cb func() error) { + if cb == nil { + return + } + + var executed uint32 + + callback := func() error { + if atomic.CompareAndSwapUint32(&executed, 0, 1) { + return cb() + } + + return nil + } + + ctx.OnConnectionCloseErr(callback) + + onFlush := func() { + if err := callback(); err != nil { + // Can be ignored. + ctx.app.Logger().Debugf("OnClose: SetBeforeFlush: received error: %v", err) + } + } + + ctx.writer.SetBeforeFlush(onFlush) +} + /* Note(@kataras): just leave end-developer decide. const goroutinesContextKey = "iris.goroutines" @@ -795,7 +854,7 @@ func GetHost(r *http.Request) string { // Subdomain returns the subdomain of this request, if any. // Note that this is a fast method which does not cover all cases. func (ctx *Context) Subdomain() (subdomain string) { - host := ctx.request.URL.Host // ctx.Host() + host := ctx.Host() if index := strings.IndexByte(host, '.'); index > 0 { subdomain = host[0:index] } diff --git a/go.mod b/go.mod index ac30eb3f..fb951704 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/ryanuber/columnize v2.1.0+incompatible github.com/schollz/closestmatch v2.1.0+incompatible github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 + github.com/tdewolff/minify/v2 v2.8.0 github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 github.com/yosssi/ace v0.0.5 go.etcd.io/bbolt v1.3.5 diff --git a/iris.go b/iris.go index a5f733db..036a96b2 100644 --- a/iris.go +++ b/iris.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "os" + "regexp" "strings" "sync" "time" @@ -25,6 +26,14 @@ import ( "github.com/kataras/golog" "github.com/kataras/tunnel" + + "github.com/tdewolff/minify/v2" + "github.com/tdewolff/minify/v2/css" + "github.com/tdewolff/minify/v2/html" + "github.com/tdewolff/minify/v2/js" + "github.com/tdewolff/minify/v2/json" + "github.com/tdewolff/minify/v2/svg" + "github.com/tdewolff/minify/v2/xml" ) // Version is the current version number of the Iris Web Framework. @@ -65,6 +74,8 @@ type Application struct { // Validator is the request body validator, defaults to nil. Validator context.Validator + // Minifier to minify responses. + minifier *minify.M // view engine view view.View @@ -92,6 +103,7 @@ func New() *Application { app := &Application{ config: &config, logger: golog.Default, + minifier: newMinifier(), I18n: i18n.New(), APIBuilder: router.NewAPIBuilder(), Router: router.NewRouter(), @@ -250,6 +262,28 @@ func (app *Application) Validate(v interface{}) error { return nil } +func newMinifier() *minify.M { + m := minify.New() + m.AddFunc("text/css", css.Minify) + m.AddFunc("text/html", html.Minify) + m.AddFunc("image/svg+xml", svg.Minify) + m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify) + m.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify) + m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify) + return m +} + +// Minifier returns the minifier instance. +// By default it can minifies: +// - text/html +// - text/css +// - image/svg+xml +// - application/text(javascript, ecmascript, json, xml). +// Use that instance to add custom Minifiers before server ran. +func (app *Application) Minifier() *minify.M { + return app.minifier +} + // RegisterView should be used to register view engines mapping to a root directory // and the template file(s) extension. func (app *Application) RegisterView(viewEngine view.Engine) {