diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f3091..b06ba91 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.2.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index b2181d4..216b112 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 66 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/calvinfo-o4h6u5/cerca-b1933e28ba1d2a81cae6514fd41ec2ac5289660666174bfa4466d456e1740fb1.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/calvinfo-o4h6u5/cerca-f1c349d486ace3b0f8cfb765e3d56b561aac2cab99d5a87fd795c7d74db68311.yml openapi_spec_hash: d1e63b49a56f6c27dc3dac475c34d612 -config_hash: 411e4c3ec8f57219c56018ea49c09614 +config_hash: f6565c46c739d01060c1217b6a22045e diff --git a/CHANGELOG.md b/CHANGELOG.md index 196e94b..a14756c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.2.1 (2026-05-08) + +Full Changelog: [v0.2.0...v0.2.1](https://github.com/matrices/cerca-go/compare/v0.2.0...v0.2.1) + +### Bug Fixes + +* **go:** avoid panic when http.DefaultTransport is wrapped ([5e9bf58](https://github.com/matrices/cerca-go/commit/5e9bf580e3d7bafd21fd8d60e14c22b37498f963)) + + +### Chores + +* redact api-key headers in debug logs ([15caa73](https://github.com/matrices/cerca-go/commit/15caa732754d2ec0410724dea21b388831dbaf33)) + ## 0.2.0 (2026-05-06) Full Changelog: [v0.1.0...v0.2.0](https://github.com/matrices/cerca-go/compare/v0.1.0...v0.2.0) diff --git a/README.md b/README.md index 6ed1e74..0df352f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Or to pin the version: ```sh -go get -u 'github.com/matrices/cerca-go@v0.2.0' +go get -u 'github.com/matrices/cerca-go@v0.2.1' ``` diff --git a/default_http_client.go b/default_http_client.go index f729243..9e94a9e 100644 --- a/default_http_client.go +++ b/default_http_client.go @@ -14,11 +14,17 @@ import ( const defaultResponseHeaderTimeout = 10 * time.Minute // defaultHTTPClient returns an [*http.Client] used when the caller does not -// supply one via [option.WithHTTPClient]. It clones [http.DefaultTransport] -// and adds a [http.Transport.ResponseHeaderTimeout] so stuck connections -// fail fast instead of compounding across retries. +// supply one via [option.WithHTTPClient]. When [http.DefaultTransport] is the +// stdlib [*http.Transport], it is cloned and a [http.Transport.ResponseHeaderTimeout] +// is set so stuck connections fail fast instead of compounding across retries. +// If [http.DefaultTransport] has been wrapped (for example by otelhttp for +// distributed tracing), the wrapping is preserved and the header timeout is +// skipped. func defaultHTTPClient() *http.Client { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout - return &http.Client{Transport: transport} + if t, ok := http.DefaultTransport.(*http.Transport); ok { + t = t.Clone() + t.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: t} + } + return &http.Client{Transport: http.DefaultTransport} } diff --git a/internal/version.go b/internal/version.go index 774c6c4..a230145 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.2.0" // x-release-please-version +const PackageVersion = "0.2.1" // x-release-please-version diff --git a/option/middleware.go b/option/middleware.go index 8ec9dd6..4be0987 100644 --- a/option/middleware.go +++ b/option/middleware.go @@ -8,6 +8,10 @@ import ( "net/http/httputil" ) +// sensitiveLogHeaders are redacted before request and response content is +// written to the debug logger. +var sensitiveLogHeaders = []string{"authorization", "api-key", "x-api-key", "cookie", "set-cookie"} + // WithDebugLog logs the HTTP request and response content. // If the logger parameter is nil, it uses the default logger. // @@ -20,7 +24,7 @@ func WithDebugLog(logger *log.Logger) RequestOption { logger = log.Default() } - if reqBytes, err := httputil.DumpRequest(req, true); err == nil { + if reqBytes, err := dumpRedactedRequest(req); err == nil { logger.Printf("Request Content:\n%s\n", reqBytes) } @@ -29,10 +33,48 @@ func WithDebugLog(logger *log.Logger) RequestOption { return resp, err } - if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + if respBytes, err := dumpRedactedResponse(resp); err == nil { logger.Printf("Response Content:\n%s\n", respBytes) } return resp, err }) } + +// dumpRedactedRequest dumps req with sensitive headers replaced. The +// original headers are restored via defer so a panic in DumpRequest cannot +// leak the placeholder map into the live request sent downstream. +func dumpRedactedRequest(req *http.Request) ([]byte, error) { + origHeaders := req.Header + req.Header = redactDebugHeaders(origHeaders) + defer func() { req.Header = origHeaders }() + return httputil.DumpRequest(req, true) +} + +func dumpRedactedResponse(resp *http.Response) ([]byte, error) { + origHeaders := resp.Header + resp.Header = redactDebugHeaders(origHeaders) + defer func() { resp.Header = origHeaders }() + return httputil.DumpResponse(resp, true) +} + +func redactDebugHeaders(headers http.Header) http.Header { + var redacted http.Header + for _, name := range sensitiveLogHeaders { + values := headers.Values(name) + if len(values) == 0 { + continue + } + if redacted == nil { + redacted = headers.Clone() + } + redacted.Del(name) + for range values { + redacted.Add(name, "***") + } + } + if redacted == nil { + return headers + } + return redacted +}