Keep only redacted text in typed errors, not the credential
CI / ci (push) Successful in 1m28s
Independent review Codex gpt-6-sol high: APPROVED (0 findings)
CI / ci (pull_request) Successful in 1m27s

ProviderError and RateLimitedError now build their redacted Error text
when constructed and do not store the secret, so formatting the value
with %#v or %+v cannot print it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 07:38:03 -04:00
co-authored by Claude Opus 5.5
parent 893e00d14e
commit fba7667949
2 changed files with 38 additions and 18 deletions
+26 -18
View File
@@ -32,22 +32,30 @@ type RateLimitedError struct {
// RetryAfter is how long the vendor asked callers to wait; 0 means it
// did not say.
RetryAfter time.Duration
secret string
// text is the redacted Error text set by NewRateLimitedError; "" means
// the error was built without a credential.
text string
}
// NewRateLimitedError returns the error for a throttled request. secret is
// the credential sent with the request, or "" when the request sent none;
// Error removes every occurrence of a non-empty secret, even where the
// fixed text happens to spell it.
// the credential sent with the request, or "" when the request sent none.
// Every occurrence of a non-empty secret is removed from the error text
// when it is built, even where the fixed text happens to spell it, and the
// secret itself is not kept.
func NewRateLimitedError(retryAfter time.Duration, secret string) *RateLimitedError {
return &RateLimitedError{RetryAfter: retryAfter, secret: secret}
e := &RateLimitedError{RetryAfter: retryAfter}
e.text = Redact(e.Error(), secret)
return e
}
func (e *RateLimitedError) Error() string {
if e.RetryAfter == 0 {
return Redact(ErrRateLimited.Error(), e.secret)
if e.text != "" {
return e.text
}
return Redact(fmt.Sprintf("%s: retry after %v", ErrRateLimited, e.RetryAfter), e.secret)
if e.RetryAfter == 0 {
return ErrRateLimited.Error()
}
return fmt.Sprintf("%s: retry after %v", ErrRateLimited, e.RetryAfter)
}
// Is reports whether target is ErrRateLimited.
@@ -84,7 +92,7 @@ func Redact(s, secret string) string {
type ProviderError struct {
status int
body string
secret string
text string
}
// NewProviderError returns the error for an unexpected HTTP status. It
@@ -92,8 +100,9 @@ type ProviderError struct {
// credential sent with the request, or "" when the request sent none; every
// occurrence of a non-empty secret in body is removed by Redact before the
// body is cut, so no part of it survives, and again after the cut, so the
// truncation mark cannot complete it. Error removes it from the complete
// text too, even where the fixed text happens to spell it.
// truncation mark cannot complete it. It is removed from the complete error
// text too, even where the fixed text happens to spell it, and the secret
// itself is not kept.
func NewProviderError(status int, body []byte, secret string) *ProviderError {
s := strings.TrimSpace(Redact(string(body), secret))
if len(s) > MaxBodyExcerpt {
@@ -103,7 +112,11 @@ func NewProviderError(status int, body []byte, secret string) *ProviderError {
}
s = Redact(s[:cut]+"…", secret)
}
return &ProviderError{status: status, body: s, secret: secret}
text := fmt.Sprintf("%s: HTTP %d", ErrProvider, status)
if s != "" {
text += ": " + s
}
return &ProviderError{status: status, body: s, text: Redact(text, secret)}
}
// Status returns the HTTP status code.
@@ -113,12 +126,7 @@ func (e *ProviderError) Status() int { return e.status }
// when the vendor sent no body.
func (e *ProviderError) Body() string { return e.body }
func (e *ProviderError) Error() string {
if e.body == "" {
return Redact(fmt.Sprintf("%s: HTTP %d", ErrProvider, e.status), e.secret)
}
return Redact(fmt.Sprintf("%s: HTTP %d: %s", ErrProvider, e.status, e.body), e.secret)
}
func (e *ProviderError) Error() string { return e.text }
// Is reports whether target is ErrProvider.
func (e *ProviderError) Is(target error) bool { return target == ErrProvider }
+12
View File
@@ -184,3 +184,15 @@ func TestRateLimitedErrorWithFixedWordSecretNeverLeaksSecret(t *testing.T) {
}
}
}
func TestTypedErrorsDoNotKeepSecret(t *testing.T) {
const secret = "sk-live-0123456789abcdef"
for _, err := range []error{
NewProviderError(500, []byte("down "+secret), secret),
NewRateLimitedError(30*time.Second, secret),
} {
if got := fmt.Sprintf("%#v %+v", err, err); strings.Contains(got, secret) {
t.Fatalf("formatting %T leaks the secret: %q", err, got)
}
}
}