Skip to content

Commit a8f4210

Browse files
committed
feat(compression): add Brotli support for compression/decompression
- Enhanced HAR translation to include `_resourceType` for XHR detection.
1 parent 49fc264 commit a8f4210

7 files changed

Lines changed: 151 additions & 11 deletions

File tree

packages/server/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
)
1717

1818
require (
19+
github.com/andybalholm/brotli v1.1.1 // indirect
1920
github.com/google/go-cmp v0.6.0 // indirect
2021
github.com/shopspring/decimal v1.4.0 // indirect
2122
golang.org/x/text v0.23.0 // indirect

packages/server/go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU
44
github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
55
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
66
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
7+
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
8+
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
79
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
810
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
911
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
@@ -20,6 +22,7 @@ github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU
2022
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
2123
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
2224
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
25+
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
2326
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
2427
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
2528
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=

packages/server/pkg/compress/compress.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"io"
88
"the-dev-tools/server/pkg/zstdcompress"
9+
10+
"github.com/andybalholm/brotli"
911
)
1012

1113
type CompressType int8
@@ -14,8 +16,16 @@ const (
1416
CompressTypeNone CompressType = 0
1517
CompressTypeGzip CompressType = 1
1618
CompressTypeZstd CompressType = 2
19+
CompressTypeBr CompressType = 3
1720
)
1821

22+
var CompressLockupMap map[string]CompressType = map[string]CompressType{
23+
"": CompressTypeNone,
24+
"gzip": CompressTypeGzip,
25+
"zstd": CompressTypeZstd,
26+
"br": CompressTypeBr,
27+
}
28+
1929
// TODO: refactor this for better performance
2030
func Compress(data []byte, compressType CompressType) ([]byte, error) {
2131
var buf bytes.Buffer
@@ -31,10 +41,20 @@ func Compress(data []byte, compressType CompressType) ([]byte, error) {
3141
if err != nil {
3242
return nil, err
3343
}
34-
3544
case CompressTypeZstd:
3645
byteArr := zstdcompress.Compress(data)
3746
buf.Write(byteArr)
47+
case CompressTypeBr:
48+
// compress data with brotli
49+
w := brotli.NewWriter(&buf)
50+
_, err := w.Write(data)
51+
if err != nil {
52+
return nil, err
53+
}
54+
err = w.Close()
55+
if err != nil {
56+
return nil, err
57+
}
3858
}
3959
return buf.Bytes(), nil
4060
}
@@ -58,8 +78,20 @@ func Decompress(data []byte, compressType CompressType) ([]byte, error) {
5878

5979
case CompressTypeZstd:
6080
return zstdcompress.Decompress(data)
61-
81+
case CompressTypeBr:
82+
// decompress data with brotli
83+
br := brotli.NewReader(&buf)
84+
return io.ReadAll(br)
6285
default:
6386
return nil, fmt.Errorf("unsupported compression type: %v", compressType)
6487
}
6588
}
89+
90+
func DecompressWithContentEncodeStr(data []byte, contentEncoding string) ([]byte, error) {
91+
compressType, ok := CompressLockupMap[contentEncoding]
92+
if !ok {
93+
return nil, fmt.Errorf("%s encoding not supported", contentEncoding)
94+
}
95+
96+
return Decompress(data, compressType)
97+
}

packages/server/pkg/http/request/request.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ func PrepareRequest(endpoint mitemapi.ItemApi, example mitemapiexample.ItemApiEx
9494
compressType = compress.CompressTypeGzip
9595
case "zstd":
9696
compressType = compress.CompressTypeZstd
97-
case "deflate", "br", "identity":
97+
case "br":
98+
compressType = compress.CompressTypeBr
99+
case "deflate", "identity":
98100
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("%s not supported", header.Value))
99101
default:
100102
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid compression type %s", header.Value))

packages/server/pkg/httpclient/httpclient.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package httpclient
33
import (
44
"bytes"
55
"encoding/json"
6-
"fmt"
76
"io"
87
"net/http"
98
"net/url"
@@ -99,16 +98,11 @@ func SendRequestAndConvert(client HttpClient, req *Request, exampleID idwrap.IDW
9998
}
10099

101100
encoding := resp.Header.Get("Content-Encoding")
102-
switch encoding {
103-
case "gzip":
104-
data, err := compress.Decompress(body, compress.CompressTypeGzip)
101+
if encoding != "" {
102+
body, err = compress.DecompressWithContentEncodeStr(body, encoding)
105103
if err != nil {
106104
return Response{}, err
107105
}
108-
body = data
109-
case "":
110-
default:
111-
return Response{}, fmt.Errorf("not support Content-Encoding: %s", encoding)
112106
}
113107

114108
err = resp.Body.Close()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package httpclient_test
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"reflect"
7+
"testing"
8+
"the-dev-tools/server/pkg/httpclient"
9+
"the-dev-tools/server/pkg/model/mexamplerespheader"
10+
)
11+
12+
func TestConvertResponseToVar(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
input httpclient.Response
16+
expected httpclient.ResponseVar
17+
}{
18+
{
19+
name: "Valid JSON body",
20+
input: httpclient.Response{
21+
StatusCode: http.StatusOK,
22+
Body: []byte(`{"key": "value", "number": 123}`),
23+
Headers: []mexamplerespheader.ExampleRespHeader{
24+
{HeaderKey: "Content-Type", Value: "application/json"},
25+
{HeaderKey: "X-Request-Id", Value: "abc-123"},
26+
},
27+
},
28+
expected: httpclient.ResponseVar{
29+
StatusCode: http.StatusOK,
30+
Body: map[string]any{
31+
"key": "value",
32+
"number": json.Number("123"), // Use json.Number for comparison
33+
},
34+
Headers: map[string]string{
35+
"Content-Type": "application/json",
36+
"X-Request-Id": "abc-123",
37+
},
38+
Duration: 0, // Duration is not set by this function
39+
},
40+
},
41+
{
42+
name: "Non-JSON body",
43+
input: httpclient.Response{
44+
StatusCode: http.StatusNotFound,
45+
Body: []byte("This is plain text"),
46+
Headers: []mexamplerespheader.ExampleRespHeader{
47+
{HeaderKey: "Content-Type", Value: "text/plain"},
48+
},
49+
},
50+
expected: httpclient.ResponseVar{
51+
StatusCode: http.StatusNotFound,
52+
Body: "This is plain text",
53+
Headers: map[string]string{
54+
"Content-Type": "text/plain",
55+
},
56+
Duration: 0,
57+
},
58+
},
59+
{
60+
name: "Empty body and no headers",
61+
input: httpclient.Response{
62+
StatusCode: http.StatusNoContent,
63+
Body: []byte{},
64+
Headers: []mexamplerespheader.ExampleRespHeader{},
65+
},
66+
expected: httpclient.ResponseVar{
67+
StatusCode: http.StatusNoContent,
68+
Body: "",
69+
Headers: map[string]string{},
70+
Duration: 0,
71+
},
72+
},
73+
// Add more test cases as needed, e.g., malformed JSON
74+
}
75+
76+
for _, tt := range tests {
77+
t.Run(tt.name, func(t *testing.T) {
78+
actual := httpclient.ConvertResponseToVar(tt.input)
79+
80+
// Special handling for JSON body comparison due to potential type differences (e.g., float64 vs json.Number)
81+
if expectedBodyMap, ok := tt.expected.Body.(map[string]any); ok {
82+
if actualBodyMap, ok := actual.Body.(map[string]any); ok {
83+
// Marshal both to JSON strings for a robust comparison
84+
expectedJSON, _ := json.Marshal(expectedBodyMap)
85+
actualJSON, _ := json.Marshal(actualBodyMap)
86+
if string(expectedJSON) != string(actualJSON) {
87+
t.Errorf("ConvertResponseToVar() Body = %v, want %v", string(actualJSON), string(expectedJSON))
88+
}
89+
// Avoid comparing Body again in DeepEqual
90+
tt.expected.Body = nil
91+
actual.Body = nil
92+
} else {
93+
t.Errorf("ConvertResponseToVar() Body type mismatch: expected map[string]any, got %T", actual.Body)
94+
}
95+
}
96+
97+
if !reflect.DeepEqual(actual, tt.expected) {
98+
t.Errorf("ConvertResponseToVar() = %v, want %v", actual, tt.expected)
99+
}
100+
})
101+
}
102+
}

packages/server/pkg/translate/thar/thar.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type Log struct {
5252

5353
type Entry struct {
5454
StartedDateTime time.Time `json:"startedDateTime"`
55+
ResourceType string `json:"_resourceType"`
5556
Request Request `json:"request"`
5657
Response Response `json:"response"`
5758
}
@@ -457,6 +458,11 @@ func ConvertHAR(har *HAR, collectionID, workspaceID idwrap.IDWrap) (HarResvoled,
457458

458459
// Helper: returns true if the HAR entry is for an XHR request.
459460
func IsXHRRequest(entry Entry) bool {
461+
// Check if the entry has _resourceType set to xhr
462+
if entry.ResourceType == "xhr" {
463+
return true
464+
}
465+
460466
// Check the X-Requested-With header – common for XHR.
461467
for _, header := range entry.Request.Headers {
462468
if strings.EqualFold(header.Name, "X-Requested-With") &&

0 commit comments

Comments
 (0)