feat(14-03): tide keeps binary upstream bodies and compares every *_url upload by shape

- A response body that is not valid UTF-8 (a cover image) is written as a
  YAML !!binary scalar, never masked and replayed byte for byte
- The upload URL normalizer covers every key ending in _url (cover_url),
  not only url and thumb_url
- README and parity-testing docs updated
This commit is contained in:
Jakub Zych
2026-10-03 21:34:52 +02:00
parent 2ee97c62f6
commit 5101ecb49c
6 changed files with 115 additions and 10 deletions

View File

@@ -19,6 +19,7 @@ import (
"slices"
"strings"
"sync"
"unicode/utf8"
"github.com/goccy/go-yaml"
)
@@ -71,13 +72,49 @@ const MaxUpstreamBody = 32 << 20
// stored as a {{sha256:<hex>}} placeholder.
const upstreamHashMin = 1024
// UpstreamResponse is the recorded vendor answer the fake replays.
// UpstreamResponse is the recorded vendor answer the fake replays. A body
// that is not valid UTF-8 (an image, for example) is written as a YAML
// !!binary scalar (base64) and read back byte for byte.
type UpstreamResponse struct {
Status int `yaml:"status"`
Headers map[string]string `yaml:"headers,omitempty"`
Body string `yaml:"body,omitempty"`
}
// upstreamResponseYAML is UpstreamResponse as written: the body is a plain
// string, a binaryScalar, or nil when empty.
type upstreamResponseYAML struct {
Status int `yaml:"status"`
Headers map[string]string `yaml:"headers,omitempty"`
Body any `yaml:"body,omitempty"`
}
// MarshalYAML writes a non-UTF-8 body as !!binary so the bytes survive the
// round trip.
func (r UpstreamResponse) MarshalYAML() (any, error) {
return upstreamResponseYAML{Status: r.Status, Headers: r.Headers, Body: yamlBody(r.Body)}, nil
}
// binaryScalar is a byte string written as a YAML !!binary scalar.
type binaryScalar string
// MarshalYAML implements yaml.BytesMarshaler.
func (b binaryScalar) MarshalYAML() ([]byte, error) {
return []byte("!!binary " + base64.StdEncoding.EncodeToString([]byte(b))), nil
}
// yamlBody is the value written for a body: nil when empty, a binaryScalar
// when it is not valid UTF-8, else the string.
func yamlBody(body string) any {
switch {
case body == "":
return nil
case !utf8.ValidString(body):
return binaryScalar(body)
}
return body
}
// UpstreamCompareHeaders are the request headers the fake asserts. A header
// is compared when either the recorded or the sent request carries it.
var UpstreamCompareHeaders = []string{
@@ -305,9 +342,13 @@ func (f *UpstreamFake) response(r UpstreamResponse, req *http.Request) (*http.Re
}
h.Set(k, ev)
}
body, err := f.store.Expand(r.Body)
if err != nil {
return nil, fmt.Errorf("response body: %w", err)
body := r.Body
if utf8.ValidString(body) {
// A binary body is replayed byte for byte, never expanded.
var err error
if body, err = f.store.Expand(body); err != nil {
return nil, fmt.Errorf("response body: %w", err)
}
}
return &http.Response{
Status: fmt.Sprintf("%d %s", r.Status, http.StatusText(r.Status)),
@@ -598,7 +639,11 @@ func maskUpstream(s UpstreamSidecar, store *Store) (UpstreamSidecar, error) {
}
resp := ex.Response
resp.Headers = scrubMap(resp.Headers, pairs, true)
resp.Body = replaceAll(resp.Body, pairs, false)
if utf8.ValidString(resp.Body) {
// A binary body (an image) is kept byte for byte: masking a
// short variable value inside it would corrupt the file.
resp.Body = replaceAll(resp.Body, pairs, false)
}
out.Exchanges[i] = UpstreamExchange{Request: req, Response: resp}
}
return out, nil