a73x

0880e6ac

feat: decompress gzip responses in middleware save

a73x   2026-03-31 12:44

Commit message
feat: decompress gzip responses in middleware save

SaveResponse now checks Content-Encoding and decompresses gzip
bodies before writing to disk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

middleware/middleware.go
Old New
@@ -1,7 +1,10 @@
1 package middleware 1 package middleware
2 2
3 import ( 3 import (
4 "bytes"
5 "compress/gzip"
4 "fmt" 6 "fmt"
7 "io"
5 "os" 8 "os"
6 9
7 "gopkg.in/yaml.v3" 10 "gopkg.in/yaml.v3"
@@ -71,7 +74,19 @@ func (m *Middleware) Match(host, path string) *Rule {
71 } 74 }
72 75
73 // SaveResponse writes the response body to the rule's destination file, 76 // SaveResponse writes the response body to the rule's destination file,
74 // overwriting any existing content. 77 // overwriting any existing content. If contentEncoding is "gzip", the
75 func (r *Rule) SaveResponse(body []byte) error { 78 // body is decompressed before writing.
79 func (r *Rule) SaveResponse(body []byte, contentEncoding string) error {
80 if contentEncoding == "gzip" {
81 gr, err := gzip.NewReader(bytes.NewReader(body))
82 if err != nil {
83 return fmt.Errorf("decompressing gzip response: %w", err)
84 }
85 defer gr.Close()
86 body, err = io.ReadAll(gr)
87 if err != nil {
88 return fmt.Errorf("reading decompressed response: %w", err)
89 }
90 }
76 return os.WriteFile(r.Dest, body, 0644) 91 return os.WriteFile(r.Dest, body, 0644)
77 } 92 }
middleware/middleware_test.go
Old New
@@ -1,6 +1,8 @@
1 package middleware_test 1 package middleware_test
2 2
3 import ( 3 import (
4 "bytes"
5 "compress/gzip"
4 "os" 6 "os"
5 "path/filepath" 7 "path/filepath"
6 "testing" 8 "testing"
@@ -95,7 +97,7 @@ func TestSaveResponseWritesBodyToFile(t *testing.T) {
95 } 97 }
96 98
97 body := []byte(`{"tokens": 42}`) 99 body := []byte(`{"tokens": 42}`)
98 err := rule.SaveResponse(body) 100 err := rule.SaveResponse(body, "")
99 if err != nil { 101 if err != nil {
100 t.Fatalf("unexpected error: %v", err) 102 t.Fatalf("unexpected error: %v", err)
101 } 103 }
@@ -120,10 +122,35 @@ func TestSaveResponseOverwritesExistingFile(t *testing.T) {
120 } 122 }
121 123
122 body := []byte(`{"new": true}`) 124 body := []byte(`{"new": true}`)
123 rule.SaveResponse(body) 125 rule.SaveResponse(body, "")
124 126
125 got, _ := os.ReadFile(dest) 127 got, _ := os.ReadFile(dest)
126 if string(got) != string(body) { 128 if string(got) != string(body) {
127 t.Errorf("expected %q, got %q", body, got) 129 t.Errorf("expected %q, got %q", body, got)
128 } 130 }
129 } 131 }
132
133 func TestSaveResponseDecompressesGzip(t *testing.T) {
134 dest := filepath.Join(t.TempDir(), "out.json")
135 rule := &middleware.Rule{
136 Match: "example.com/data",
137 Action: "save_response",
138 Dest: dest,
139 }
140
141 original := `{"tokens": 42}`
142 var buf bytes.Buffer
143 gw := gzip.NewWriter(&buf)
144 gw.Write([]byte(original))
145 gw.Close()
146
147 err := rule.SaveResponse(buf.Bytes(), "gzip")
148 if err != nil {
149 t.Fatalf("unexpected error: %v", err)
150 }
151
152 got, _ := os.ReadFile(dest)
153 if string(got) != original {
154 t.Errorf("expected %q, got %q", original, got)
155 }
156 }
proxy/proxy.go
Old New
@@ -254,7 +254,7 @@ func (p *Proxy) handleConnectMITM(w http.ResponseWriter, r *http.Request) {
254 log.Printf("ERROR: middleware failed to read response body from %s: %v", host, err) 254 log.Printf("ERROR: middleware failed to read response body from %s: %v", host, err)
255 upstreamResp.Body = io.NopCloser(bytes.NewReader(nil)) 255 upstreamResp.Body = io.NopCloser(bytes.NewReader(nil))
256 } else { 256 } else {
257 if err := rule.SaveResponse(body); err != nil { 257 if err := rule.SaveResponse(body, upstreamResp.Header.Get("Content-Encoding")); err != nil {
258 log.Printf("ERROR: middleware failed to save response to %s: %v", rule.Dest, err) 258 log.Printf("ERROR: middleware failed to save response to %s: %v", rule.Dest, err)
259 } else { 259 } else {
260 log.Printf("MIDDLEWARE saved %s%s -> %s", host, req.URL.Path, rule.Dest) 260 log.Printf("MIDDLEWARE saved %s%s -> %s", host, req.URL.Path, rule.Dest)