a73x

0880e6ac

feat: decompress gzip responses in middleware save

a73x   2026-03-31 12:44

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>

diff --git a/middleware/middleware.go b/middleware/middleware.go
index 1e8b10e..7004aa7 100644
--- a/middleware/middleware.go
+++ b/middleware/middleware.go
@@ -1,7 +1,10 @@
package middleware

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"os"

	"gopkg.in/yaml.v3"
@@ -71,7 +74,19 @@ func (m *Middleware) Match(host, path string) *Rule {
}

// SaveResponse writes the response body to the rule's destination file,
// overwriting any existing content.
func (r *Rule) SaveResponse(body []byte) error {
// overwriting any existing content. If contentEncoding is "gzip", the
// body is decompressed before writing.
func (r *Rule) SaveResponse(body []byte, contentEncoding string) error {
	if contentEncoding == "gzip" {
		gr, err := gzip.NewReader(bytes.NewReader(body))
		if err != nil {
			return fmt.Errorf("decompressing gzip response: %w", err)
		}
		defer gr.Close()
		body, err = io.ReadAll(gr)
		if err != nil {
			return fmt.Errorf("reading decompressed response: %w", err)
		}
	}
	return os.WriteFile(r.Dest, body, 0644)
}
diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go
index 5c2b170..952926c 100644
--- a/middleware/middleware_test.go
+++ b/middleware/middleware_test.go
@@ -1,6 +1,8 @@
package middleware_test

import (
	"bytes"
	"compress/gzip"
	"os"
	"path/filepath"
	"testing"
@@ -95,7 +97,7 @@ func TestSaveResponseWritesBodyToFile(t *testing.T) {
	}

	body := []byte(`{"tokens": 42}`)
	err := rule.SaveResponse(body)
	err := rule.SaveResponse(body, "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
@@ -120,10 +122,35 @@ func TestSaveResponseOverwritesExistingFile(t *testing.T) {
	}

	body := []byte(`{"new": true}`)
	rule.SaveResponse(body)
	rule.SaveResponse(body, "")

	got, _ := os.ReadFile(dest)
	if string(got) != string(body) {
		t.Errorf("expected %q, got %q", body, got)
	}
}

func TestSaveResponseDecompressesGzip(t *testing.T) {
	dest := filepath.Join(t.TempDir(), "out.json")
	rule := &middleware.Rule{
		Match:  "example.com/data",
		Action: "save_response",
		Dest:   dest,
	}

	original := `{"tokens": 42}`
	var buf bytes.Buffer
	gw := gzip.NewWriter(&buf)
	gw.Write([]byte(original))
	gw.Close()

	err := rule.SaveResponse(buf.Bytes(), "gzip")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	got, _ := os.ReadFile(dest)
	if string(got) != original {
		t.Errorf("expected %q, got %q", original, got)
	}
}
diff --git a/proxy/proxy.go b/proxy/proxy.go
index 0b6ad7c..8dc4d25 100644
--- a/proxy/proxy.go
+++ b/proxy/proxy.go
@@ -254,7 +254,7 @@ func (p *Proxy) handleConnectMITM(w http.ResponseWriter, r *http.Request) {
					log.Printf("ERROR: middleware failed to read response body from %s: %v", host, err)
					upstreamResp.Body = io.NopCloser(bytes.NewReader(nil))
				} else {
					if err := rule.SaveResponse(body); err != nil {
					if err := rule.SaveResponse(body, upstreamResp.Header.Get("Content-Encoding")); err != nil {
						log.Printf("ERROR: middleware failed to save response to %s: %v", rule.Dest, err)
					} else {
						log.Printf("MIDDLEWARE saved %s%s -> %s", host, req.URL.Path, rule.Dest)