Keep %#v of every LLM adapter error free of the API key (MQ-03)
CI / ci (push) Successful in 1m29s
Independent review Codex gpt-6-sol high: APPROVED (0 findings)
CI / ci (pull_request) Successful in 1m28s

%#v prints struct field names, so a key that spells one (for example
"body" on ProviderError) leaked even though Error() was redacted.

- llm.ProviderError and llm.RateLimitedError gain GoString returning
  their already-redacted Error text.
- Both adapters' transportFailure now holds its fully redacted text and
  gains GoString.
- The redact backstop now wraps every non-nil error when a key is set,
  so the returned error and every unwrapped layer print through
  redactedError.GoString, never a raw struct such as fmt.wrapError.
- redactedError.As also refuses a match whose %#v form holds the key.
- Contract test in both adapters: field-name keys (body, status, text,
  RetryAfter, msg, err, key, kind) on HTTP 500 "down" and 429, checking
  %#v of the returned error, every unwrapped layer and errors.As results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 08:05:17 -04:00
co-authored by Claude Opus 5.5
parent cdf59c4eee
commit d11cdbbefb
5 changed files with 160 additions and 41 deletions
+7
View File
@@ -58,6 +58,9 @@ func (e *RateLimitedError) Error() string {
return fmt.Sprintf("%s: retry after %v", ErrRateLimited, e.RetryAfter)
}
// GoString keeps %#v to the redacted Error text instead of the fields.
func (e *RateLimitedError) GoString() string { return e.Error() }
// Is reports whether target is ErrRateLimited.
func (e *RateLimitedError) Is(target error) bool { return target == ErrRateLimited }
@@ -128,5 +131,9 @@ func (e *ProviderError) Body() string { return e.body }
func (e *ProviderError) Error() string { return e.text }
// GoString keeps %#v to the redacted Error text instead of the fields,
// whose names could spell the credential.
func (e *ProviderError) GoString() string { return e.text }
// Is reports whether target is ErrProvider.
func (e *ProviderError) Is(target error) bool { return target == ErrProvider }