Handle gzip HTML proxy injection (#876)

* Handle gzip HTML proxy injection

* Avoid require in gzip handler test

* Run go mod tidy
This commit is contained in:
xiantang
2026-01-23 23:18:25 +08:00
committed by GitHub
parent 6cbf191523
commit 80b34bcfa1
3 changed files with 85 additions and 8 deletions
+1 -1
View File
@@ -10,6 +10,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/pelletier/go-toml v1.9.5
github.com/stretchr/testify v1.11.1
golang.org/x/sys v0.35.0
)
require (
@@ -27,7 +28,6 @@ require (
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/tdewolff/parse/v2 v2.8.3 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+43 -6
View File
@@ -2,6 +2,7 @@ package runner
import (
"bytes"
"compress/gzip"
"context"
_ "embed"
"fmt"
@@ -69,21 +70,33 @@ func (p *Proxy) BuildFailed(msg BuildFailedMsg) {
p.stream.BuildFailed(msg)
}
func (p *Proxy) injectLiveReload(resp *http.Response) (string, error) {
func (p *Proxy) injectLiveReload(resp *http.Response) (string, bool, error) {
reader := resp.Body
decodedGzip := false
if isGzipEncoded(resp.Header) {
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return "", false, fmt.Errorf("proxy inject: failed to init gzip reader: %w", err)
}
defer gzipReader.Close()
reader = gzipReader
decodedGzip = true
}
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(resp.Body); err != nil {
return "", fmt.Errorf("proxy inject: failed to read body from http response")
if _, err := buf.ReadFrom(reader); err != nil {
return "", decodedGzip, fmt.Errorf("proxy inject: failed to read body from http response: %w", err)
}
page := buf.String()
// the script will be injected before the end of the body tag. In case the tag is missing, the injection will be skipped with no error.
body := strings.LastIndex(page, "</body>")
if body == -1 {
return page, nil
return page, decodedGzip, nil
}
script := "<script>" + ProxyScript + "</script>"
return page[:body] + script + page[body:], nil
return page[:body] + script + page[body:], decodedGzip, nil
}
func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) {
@@ -194,11 +207,14 @@ func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) {
}
} else {
// HTML: inject live reload script
page, err := p.injectLiveReload(resp)
page, decodedGzip, err := p.injectLiveReload(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if decodedGzip {
w.Header().Del("Content-Encoding")
}
w.Header().Set("Content-Length", strconv.Itoa((len([]byte(page)))))
w.WriteHeader(resp.StatusCode)
if _, err := io.WriteString(w, page); err != nil {
@@ -208,6 +224,27 @@ func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) {
}
}
func isGzipEncoded(header http.Header) bool {
encoding := header.Get("Content-Encoding")
if encoding == "" {
return false
}
hasGzip := false
for _, value := range strings.Split(encoding, ",") {
trimmed := strings.TrimSpace(strings.ToLower(value))
if trimmed == "" {
continue
}
if trimmed != "gzip" && trimmed != "x-gzip" {
return false
}
hasGzip = true
}
return hasGzip
}
func (p *Proxy) reloadHandler(w http.ResponseWriter, r *http.Request) {
flusher, err := w.(http.Flusher)
if !err {
+41 -1
View File
@@ -2,6 +2,7 @@ package runner
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
@@ -213,7 +214,7 @@ func TestProxy_injectLiveReload(t *testing.T) {
ProxyPort: 1111,
AppPort: 2222,
})
got, _ := proxy.injectLiveReload(tt.given)
got, _, _ := proxy.injectLiveReload(tt.given)
if got != tt.expect {
// Use a more descriptive error message
if len(got) > 100 || len(tt.expect) > 100 {
@@ -295,6 +296,45 @@ func TestProxy_reloadHandler(t *testing.T) {
}
}
func TestProxy_proxyHandler_GzipHTML(t *testing.T) {
body := "<body><h1>gzip</h1></body>"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Encoding", "gzip")
gzipWriter := gzip.NewWriter(w)
if _, err := io.WriteString(gzipWriter, body); err != nil {
t.Errorf("write gzip body: %v", err)
}
if err := gzipWriter.Close(); err != nil {
t.Errorf("close gzip writer: %v", err)
}
}))
defer srv.Close()
srvPort := getServerPort(t, srv)
proxy := NewProxy(&cfgProxy{
Enabled: true,
ProxyPort: proxyPort,
AppPort: srvPort,
})
req := httptest.NewRequest("GET", fmt.Sprintf("http://localhost:%d/", proxyPort), nil)
req.Header.Set("Accept-Encoding", "gzip")
rec := httptest.NewRecorder()
proxy.proxyHandler(rec, req)
resp := rec.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Empty(t, resp.Header.Get("Content-Encoding"))
responseBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Contains(t, string(responseBody), ProxyScript)
}
func TestProxy_proxyHandler_SSE(t *testing.T) {
events := []string{
"event: message\ndata: {\"id\":1}\n\n",