From e351dfd799d840e18354f052b2c1a335e73bf8ec Mon Sep 17 00:00:00 2001 From: John Guo Date: Thu, 5 Dec 2024 15:35:04 +0800 Subject: [PATCH 1/2] feat(net/ghttp): add MiddlewareGzip --- net/ghttp/ghttp_middleware_gzip.go | 76 +++++++++++++ .../ghttp_z_unit_middleware_gzip_test.go | 101 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 net/ghttp/ghttp_middleware_gzip.go create mode 100644 net/ghttp/ghttp_z_unit_middleware_gzip_test.go diff --git a/net/ghttp/ghttp_middleware_gzip.go b/net/ghttp/ghttp_middleware_gzip.go new file mode 100644 index 00000000000..6599baeb461 --- /dev/null +++ b/net/ghttp/ghttp_middleware_gzip.go @@ -0,0 +1,76 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package ghttp + +import ( + "bytes" + "compress/gzip" + "net/http" + "strings" +) + +// MiddlewareGzip is a middleware that compresses HTTP response using gzip compression. +// Note that it does not compress responses if: +// 1. The response is already compressed (Content-Encoding header is set) +// 2. The client does not accept gzip compression +// 3. The response body length is too small (less than 1KB) +// +// To disable compression for specific routes, you can use the group middleware: +// +// group.Group("/api", func(group *ghttp.RouterGroup) { +// group.Middleware(ghttp.MiddlewareGzip) // Enable GZIP for /api routes +// }) +func MiddlewareGzip(r *Request) { + // Skip compression if client doesn't accept gzip + if !acceptsGzip(r.Request) { + r.Middleware.Next() + return + } + + // Execute the next handlers first + r.Middleware.Next() + + // Skip if already compressed or empty response + if r.Response.Header().Get("Content-Encoding") != "" { + return + } + + // Get the response buffer and check its length + buffer := r.Response.Buffer() + if len(buffer) < 1024 { + return + } + + // Try to compress the response + var ( + compressed bytes.Buffer + logger = r.Server.Logger() + ctx = r.Context() + ) + gzipWriter := gzip.NewWriter(&compressed) + if _, err := gzipWriter.Write(buffer); err != nil { + logger.Warningf(ctx, "gzip compression failed: %+v", err) + return + } + if err := gzipWriter.Close(); err != nil { + logger.Warningf(ctx, "gzip writer close failed: %+v", err) + return + } + + // Clear the original buffer and set headers + r.Response.ClearBuffer() + r.Response.Header().Set("Content-Encoding", "gzip") + r.Response.Header().Del("Content-Length") + + // Write the compressed data + r.Response.Write(compressed.Bytes()) +} + +// acceptsGzip returns true if the client accepts gzip compression. +func acceptsGzip(r *http.Request) bool { + return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") +} diff --git a/net/ghttp/ghttp_z_unit_middleware_gzip_test.go b/net/ghttp/ghttp_z_unit_middleware_gzip_test.go new file mode 100644 index 00000000000..02e936a86af --- /dev/null +++ b/net/ghttp/ghttp_z_unit_middleware_gzip_test.go @@ -0,0 +1,101 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package ghttp_test + +import ( + "compress/gzip" + "context" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_Middleware_Gzip(t *testing.T) { + s := g.Server(guid.S()) + + // Routes with GZIP enabled + s.Group("/", func(group *ghttp.RouterGroup) { + group.Middleware(ghttp.MiddlewareGzip) + group.ALL("/", func(r *ghttp.Request) { + r.Response.Write(strings.Repeat("Hello World! ", 1000)) + }) + group.ALL("/small", func(r *ghttp.Request) { + r.Response.Write("Small response") + }) + }) + + // Routes without GZIP + s.Group("/no-gzip", func(group *ghttp.RouterGroup) { + group.ALL("/", func(r *ghttp.Request) { + r.Response.Write(strings.Repeat("Hello World! ", 1000)) + }) + }) + + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + time.Sleep(100 * time.Millisecond) + + ctx := context.Background() + gtest.C(t, func(t *gtest.T) { + client := g.Client() + client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())) + + // Test 1: Route with GZIP, client supports GZIP + resp, err := client.Header(map[string]string{ + "Accept-Encoding": "gzip", + }).Get(ctx, "/") + t.AssertNil(err) + t.Assert(resp.Header.Get("Content-Encoding"), "gzip") + + reader, err := gzip.NewReader(resp.Body) + t.AssertNil(err) + defer reader.Close() + + content, err := io.ReadAll(reader) + t.AssertNil(err) + expected := strings.Repeat("Hello World! ", 1000) + t.Assert(len(content), len(expected)) + t.Assert(string(content), expected) + + // Test 2: Route with GZIP, client doesn't support GZIP + resp, err = client.Header(map[string]string{}).Get(ctx, "/") + t.AssertNil(err) + t.Assert(resp.Header.Get("Content-Encoding"), "") + content, err = io.ReadAll(resp.Body) + t.AssertNil(err) + t.Assert(len(content), len(expected)) + t.Assert(string(content), expected) + + // Test 3: Route with GZIP, response too small + resp, err = client.Header(map[string]string{ + "Accept-Encoding": "gzip", + }).Get(ctx, "/small") + t.AssertNil(err) + t.Assert(resp.Header.Get("Content-Encoding"), "") + content, err = io.ReadAll(resp.Body) + t.AssertNil(err) + t.Assert(string(content), "Small response") + + // Test 4: Route without GZIP + resp, err = client.Header(map[string]string{ + "Accept-Encoding": "gzip", + }).Get(ctx, "/no-gzip/") + t.AssertNil(err) + t.Assert(resp.Header.Get("Content-Encoding"), "") + content, err = io.ReadAll(resp.Body) + t.AssertNil(err) + t.Assert(string(content), expected) + }) +} From daf9cf05123d5c9aa792553fe0d2b18e152e7b0f Mon Sep 17 00:00:00 2001 From: John Guo Date: Thu, 5 Dec 2024 15:43:00 +0800 Subject: [PATCH 2/2] up --- net/ghttp/ghttp_z_unit_middleware_gzip_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/net/ghttp/ghttp_z_unit_middleware_gzip_test.go b/net/ghttp/ghttp_z_unit_middleware_gzip_test.go index 02e936a86af..7bacfe4bd75 100644 --- a/net/ghttp/ghttp_z_unit_middleware_gzip_test.go +++ b/net/ghttp/ghttp_z_unit_middleware_gzip_test.go @@ -8,7 +8,6 @@ package ghttp_test import ( "compress/gzip" - "context" "fmt" "io" "strings" @@ -23,7 +22,6 @@ import ( func Test_Middleware_Gzip(t *testing.T) { s := g.Server(guid.S()) - // Routes with GZIP enabled s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(ghttp.MiddlewareGzip) @@ -47,7 +45,6 @@ func Test_Middleware_Gzip(t *testing.T) { defer s.Shutdown() time.Sleep(100 * time.Millisecond) - ctx := context.Background() gtest.C(t, func(t *gtest.T) { client := g.Client() client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()))