Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(net/ghttp): add middleware MiddlewareGzip for compressing response content using gzip #4008

Merged
merged 2 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions net/ghttp/ghttp_middleware_gzip.go
Original file line number Diff line number Diff line change
@@ -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")
}
98 changes: 98 additions & 0 deletions net/ghttp/ghttp_z_unit_middleware_gzip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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"
"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)

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)
})
}
Loading