@@ -3,6 +3,7 @@ package record
33
44import (
55 "bytes"
6+ "encoding/base64"
67 "encoding/json"
78 "fmt"
89 "goodmock/internal/common"
@@ -33,26 +34,28 @@ type RecordedExchange struct {
3334
3435// RecordServer proxies requests to an upstream backend and records exchanges.
3536type RecordServer struct {
36- server * types.Server
37- mu sync.Mutex
38- exchanges []RecordedExchange
39- upstream string
40- client * fasthttp.Client
41- jsonContentTypes []string
42- preserveKeyOrder bool
43- sortArrayMembers bool
37+ server * types.Server
38+ mu sync.Mutex
39+ exchanges []RecordedExchange
40+ upstream string
41+ client * fasthttp.Client
42+ jsonContentTypes []string
43+ binaryContentTypes []string
44+ preserveKeyOrder bool
45+ sortArrayMembers bool
4446}
4547
4648// NewRecordServer creates a new recording proxy server.
47- func NewRecordServer (upstream , proxyHost , refererPath string , verbose bool , jsonContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) * RecordServer {
49+ func NewRecordServer (upstream , proxyHost , refererPath string , verbose bool , jsonContentTypes , binaryContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) * RecordServer {
4850 return & RecordServer {
49- server : server .NewServer (proxyHost , refererPath , verbose ),
50- exchanges : make ([]RecordedExchange , 0 ),
51- upstream : upstream ,
52- client : & fasthttp.Client {},
53- jsonContentTypes : jsonContentTypes ,
54- preserveKeyOrder : preserveKeyOrder ,
55- sortArrayMembers : sortArrayMembers ,
51+ server : server .NewServer (proxyHost , refererPath , verbose , nil ),
52+ exchanges : make ([]RecordedExchange , 0 ),
53+ upstream : upstream ,
54+ client : & fasthttp.Client {},
55+ jsonContentTypes : jsonContentTypes ,
56+ binaryContentTypes : binaryContentTypes ,
57+ preserveKeyOrder : preserveKeyOrder ,
58+ sortArrayMembers : sortArrayMembers ,
5659 }
5760}
5861
@@ -208,11 +211,11 @@ func handleSnapshot(rs *RecordServer, ctx *fasthttp.RequestCtx) {
208211 // as [] not null (Cypress spreads this array and null is not iterable)
209212 mappings := make ([]types.Mapping , 0 )
210213 if snapReq .RepeatsAsScenarios {
211- if m := exchangesToScenarioMappings (filtered , rs .jsonContentTypes , rs .preserveKeyOrder , rs .sortArrayMembers ); m != nil {
214+ if m := exchangesToScenarioMappings (filtered , rs .jsonContentTypes , rs .binaryContentTypes , rs . preserveKeyOrder , rs .sortArrayMembers ); m != nil {
212215 mappings = m
213216 }
214217 } else {
215- if m := exchangesToMappings (filtered , rs .jsonContentTypes , rs .preserveKeyOrder , rs .sortArrayMembers ); m != nil {
218+ if m := exchangesToMappings (filtered , rs .jsonContentTypes , rs .binaryContentTypes , rs . preserveKeyOrder , rs .sortArrayMembers ); m != nil {
216219 mappings = m
217220 }
218221 }
@@ -230,7 +233,7 @@ func handleSnapshot(rs *RecordServer, ctx *fasthttp.RequestCtx) {
230233// exchangesToMappings converts exchanges to mappings, deduplicating by
231234// method + path + query params + body (keeping the last occurrence).
232235// This matches WireMock's snapshot behavior with repeatsAsScenarios=false.
233- func exchangesToMappings (exchanges []RecordedExchange , jsonContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) []types.Mapping {
236+ func exchangesToMappings (exchanges []RecordedExchange , jsonContentTypes , binaryContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) []types.Mapping {
234237 type dedupEntry struct {
235238 key string
236239 mapping types.Mapping
@@ -240,7 +243,7 @@ func exchangesToMappings(exchanges []RecordedExchange, jsonContentTypes []string
240243 var entries []dedupEntry
241244
242245 for _ , ex := range exchanges {
243- m := exchangeToMapping (ex , jsonContentTypes , preserveKeyOrder , sortArrayMembers )
246+ m := exchangeToMapping (ex , jsonContentTypes , binaryContentTypes , preserveKeyOrder , sortArrayMembers )
244247 key := deduplicationKey (m )
245248
246249 if idx , exists := seen [key ]; exists {
@@ -286,7 +289,7 @@ func deduplicationKey(m types.Mapping) string {
286289}
287290
288291// exchangesToScenarioMappings converts exchanges to mappings, creating scenarios for repeated URLs.
289- func exchangesToScenarioMappings (exchanges []RecordedExchange , jsonContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) []types.Mapping {
292+ func exchangesToScenarioMappings (exchanges []RecordedExchange , jsonContentTypes , binaryContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) []types.Mapping {
290293 // Group by URL+method
291294 type group struct {
292295 key string
@@ -311,12 +314,12 @@ func exchangesToScenarioMappings(exchanges []RecordedExchange, jsonContentTypes
311314 g := groups [key ]
312315 if len (g .exchanges ) == 1 {
313316 // Single occurrence — no scenario needed
314- mappings = append (mappings , exchangeToMapping (g .exchanges [0 ], jsonContentTypes , preserveKeyOrder , sortArrayMembers ))
317+ mappings = append (mappings , exchangeToMapping (g .exchanges [0 ], jsonContentTypes , binaryContentTypes , preserveKeyOrder , sortArrayMembers ))
315318 } else {
316319 // Multiple occurrences — create scenario chain
317320 scenarioName := generateMappingName (g .exchanges [0 ].URL )
318321 for i , ex := range g .exchanges {
319- m := exchangeToMapping (ex , jsonContentTypes , preserveKeyOrder , sortArrayMembers )
322+ m := exchangeToMapping (ex , jsonContentTypes , binaryContentTypes , preserveKeyOrder , sortArrayMembers )
320323 m .ScenarioName = scenarioName
321324 if i == 0 {
322325 m .RequiredScenarioState = "Started"
@@ -346,7 +349,7 @@ func sortMappings(mappings []types.Mapping) {
346349}
347350
348351// exchangeToMapping converts a recorded exchange to a WireMock mapping.
349- func exchangeToMapping (ex RecordedExchange , jsonContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) types.Mapping {
352+ func exchangeToMapping (ex RecordedExchange , jsonContentTypes , binaryContentTypes []string , preserveKeyOrder , sortArrayMembers bool ) types.Mapping {
350353 // Split URL into path and query parameters
351354 rawPath := ex .URL
352355 var queryString string
@@ -443,8 +446,10 @@ func exchangeToMapping(ex RecordedExchange, jsonContentTypes []string, preserveK
443446 Headers : headers ,
444447 }
445448
446- // Store as structured JSON if Content-Type matches, otherwise as string
447- if isJSONContentType (ex .RespHeaders , jsonContentTypes ) {
449+ // Store as base64 if binary Content-Type, structured JSON if JSON Content-Type, otherwise as string
450+ if isContentType (ex .RespHeaders , binaryContentTypes ) {
451+ resp .Body = base64 .StdEncoding .EncodeToString (ex .RespBody )
452+ } else if isJSONContentType (ex .RespHeaders , jsonContentTypes ) {
448453 if preserveKeyOrder && ! sortArrayMembers {
449454 // Use json.RawMessage to preserve original key order from upstream
450455 var raw json.RawMessage
@@ -476,16 +481,19 @@ func exchangeToMapping(ex RecordedExchange, jsonContentTypes []string, preserveK
476481 }
477482}
478483
479- // isJSONContentType checks if the response Content-Type matches any of the given JSON types.
480- func isJSONContentType (headers map [string ][]string , jsonTypes []string ) bool {
484+ // isContentType checks if the response Content-Type matches any of the given types.
485+ func isContentType (headers map [string ][]string , contentTypes []string ) bool {
486+ if len (contentTypes ) == 0 {
487+ return false
488+ }
481489 for key , values := range headers {
482490 if ! strings .EqualFold (key , "Content-Type" ) {
483491 continue
484492 }
485493 for _ , v := range values {
486494 mediaType := strings .TrimSpace (strings .SplitN (v , ";" , 2 )[0 ])
487- for _ , jt := range jsonTypes {
488- if strings .EqualFold (mediaType , jt ) {
495+ for _ , ct := range contentTypes {
496+ if strings .EqualFold (mediaType , ct ) {
489497 return true
490498 }
491499 }
@@ -494,6 +502,11 @@ func isJSONContentType(headers map[string][]string, jsonTypes []string) bool {
494502 return false
495503}
496504
505+ // isJSONContentType checks if the response Content-Type matches any of the given JSON types.
506+ func isJSONContentType (headers map [string ][]string , jsonTypes []string ) bool {
507+ return isContentType (headers , jsonTypes )
508+ }
509+
497510// normalizeHeaderName converts a header name to HTTP canonical form (Title-Case),
498511// matching WireMock's header casing behavior.
499512func normalizeHeaderName (name string ) string {
@@ -625,9 +638,10 @@ func RunRecord() {
625638
626639 verbose := common .IsVerbose ()
627640 jsonContentTypes := common .ParseJSONContentTypes ()
641+ binaryContentTypes := common .ParseBinaryContentTypes ()
628642 preserveKeyOrder := common .PreserveJSONKeyOrder ()
629643 sortArrayMembers := common .SortArrayMembers ()
630- rs := NewRecordServer (upstream , upstream , refererPath , verbose , jsonContentTypes , preserveKeyOrder , sortArrayMembers )
644+ rs := NewRecordServer (upstream , upstream , refererPath , verbose , jsonContentTypes , binaryContentTypes , preserveKeyOrder , sortArrayMembers )
631645
632646 addr := fmt .Sprintf (":%d" , port )
633647
0 commit comments