Keep only redacted text in typed errors, not the credential
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:
+26
-18
@@ -32,22 +32,30 @@ type RateLimitedError struct {
|
|||||||
// RetryAfter is how long the vendor asked callers to wait; 0 means it
|
// RetryAfter is how long the vendor asked callers to wait; 0 means it
|
||||||
// did not say.
|
// did not say.
|
||||||
RetryAfter time.Duration
|
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
|
// NewRateLimitedError returns the error for a throttled request. secret is
|
||||||
// the credential sent with the request, or "" when the request sent none;
|
// the credential sent with the request, or "" when the request sent none.
|
||||||
// Error removes every occurrence of a non-empty secret, even where the
|
// Every occurrence of a non-empty secret is removed from the error text
|
||||||
// fixed text happens to spell it.
|
// 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 {
|
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 {
|
func (e *RateLimitedError) Error() string {
|
||||||
if e.RetryAfter == 0 {
|
if e.text != "" {
|
||||||
return Redact(ErrRateLimited.Error(), e.secret)
|
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.
|
// Is reports whether target is ErrRateLimited.
|
||||||
@@ -84,7 +92,7 @@ func Redact(s, secret string) string {
|
|||||||
type ProviderError struct {
|
type ProviderError struct {
|
||||||
status int
|
status int
|
||||||
body string
|
body string
|
||||||
secret string
|
text string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProviderError returns the error for an unexpected HTTP status. It
|
// 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
|
// 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
|
// 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
|
// 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
|
// truncation mark cannot complete it. It is removed from the complete error
|
||||||
// text too, even where the fixed text happens to spell it.
|
// 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 {
|
func NewProviderError(status int, body []byte, secret string) *ProviderError {
|
||||||
s := strings.TrimSpace(Redact(string(body), secret))
|
s := strings.TrimSpace(Redact(string(body), secret))
|
||||||
if len(s) > MaxBodyExcerpt {
|
if len(s) > MaxBodyExcerpt {
|
||||||
@@ -103,7 +112,11 @@ func NewProviderError(status int, body []byte, secret string) *ProviderError {
|
|||||||
}
|
}
|
||||||
s = Redact(s[:cut]+"…", secret)
|
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.
|
// Status returns the HTTP status code.
|
||||||
@@ -113,12 +126,7 @@ func (e *ProviderError) Status() int { return e.status }
|
|||||||
// when the vendor sent no body.
|
// when the vendor sent no body.
|
||||||
func (e *ProviderError) Body() string { return e.body }
|
func (e *ProviderError) Body() string { return e.body }
|
||||||
|
|
||||||
func (e *ProviderError) Error() string {
|
func (e *ProviderError) Error() string { return e.text }
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is reports whether target is ErrProvider.
|
// Is reports whether target is ErrProvider.
|
||||||
func (e *ProviderError) Is(target error) bool { return target == ErrProvider }
|
func (e *ProviderError) Is(target error) bool { return target == ErrProvider }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user