Files
3x-ui/internal/web/middleware/gzip_static_test.go
T
MHSanaei 3526a65f5d fix(web): bound the panel's open connections and share asset gzip
The engine-wide gzip middleware gave every in-flight request its own
compressor of about 1 MiB, and the public /assets bundles let an
anonymous client keep a handler blocked in Write until WriteTimeout, so
a thousand stalled downloads held a gigabyte. The listener itself took
any number of connections, each holding buffers and a goroutine before
any login.

The bundles are now served from a gzip copy made once per file, with
the assets prefix left out of the per-request middleware, and the panel
listener closes a new connection once 2048 are open or its source (an
IPv4 address or IPv6 /64) holds 256; a loopback reverse proxy counts
only toward the total.
2026-11-01 01:01:38 +01:00

89 lines
2.7 KiB
Go

package middleware
import (
"bytes"
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"testing/fstest"
"github.com/gin-gonic/gin"
)
// readCountingFS counts ReadFile calls, i.e. how often a file gets compressed.
type readCountingFS struct {
fstest.MapFS
reads atomic.Int32
}
func (f *readCountingFS) ReadFile(name string) ([]byte, error) {
f.reads.Add(1)
return f.MapFS.ReadFile(name)
}
func TestGzipStatic(t *testing.T) {
gin.SetMode(gin.TestMode)
raw := strings.Repeat("console.log('bundle');", 4096)
files := &readCountingFS{MapFS: fstest.MapFS{
"app.js": {Data: []byte(raw)},
}}
r := gin.New()
r.Group("/assets", GzipStatic(files)).StaticFS("/", http.FS(files))
do := func(method, target, acceptEncoding string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(method, target, nil)
req.Header.Set("Accept-Encoding", acceptEncoding)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
t.Run("gzip downloads share one compression", func(t *testing.T) {
for range 3 {
w := do(http.MethodGet, "/assets/app.js", "gzip, deflate")
if w.Code != http.StatusOK || w.Header().Get("Content-Encoding") != "gzip" || w.Header().Get("Vary") != "Accept-Encoding" {
t.Fatalf("got %d, headers %v", w.Code, w.Header())
}
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "javascript") {
t.Errorf("Content-Type = %q, want the bundle's own type", ct)
}
zr, err := gzip.NewReader(bytes.NewReader(w.Body.Bytes()))
if err != nil {
t.Fatal(err)
}
if plain, _ := io.ReadAll(zr); string(plain) != raw {
t.Fatal("decompressed body differs from the bundle")
}
}
if n := files.reads.Load(); n != 1 {
t.Errorf("bundle compressed %d times, want once", n)
}
})
t.Run("HEAD sends headers only", func(t *testing.T) {
w := do(http.MethodHead, "/assets/app.js", "gzip")
if w.Code != http.StatusOK || w.Body.Len() != 0 || w.Header().Get("Content-Encoding") != "gzip" {
t.Errorf("got %d, %d body bytes, headers %v", w.Code, w.Body.Len(), w.Header())
}
})
t.Run("without Accept-Encoding the static route serves the plain file", func(t *testing.T) {
w := do(http.MethodGet, "/assets/app.js", "")
if w.Code != http.StatusOK || w.Header().Get("Content-Encoding") != "" || w.Body.String() != raw {
t.Errorf("got %d, Content-Encoding %q, %d body bytes", w.Code, w.Header().Get("Content-Encoding"), w.Body.Len())
}
})
t.Run("unknown name is left to the static route", func(t *testing.T) {
w := do(http.MethodGet, "/assets/none.js", "gzip")
if w.Code != http.StatusNotFound || w.Header().Get("Content-Encoding") != "" {
t.Errorf("got %d, Content-Encoding %q, want 404 plain", w.Code, w.Header().Get("Content-Encoding"))
}
})
}