Skip to content

Commit

Permalink
feat(net/ghttp): add middleware MiddlewareGzip for compressing resp…
Browse files Browse the repository at this point in the history
…onse content using gzip (#4008)
  • Loading branch information
gqcn authored Dec 9, 2024
1 parent 80e73da commit bae78fb
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 0 deletions.
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)
})
}

0 comments on commit bae78fb

Please # to comment.