Redact recipe-candidate failures in logs
CI / ci (push) Successful in 2m29s
CI / ci (pull_request) Successful in 2m41s

A recipe-candidates request whose stored-recipe lookup failed logged the
error unchanged, and that error quotes the search result's URL, which can
carry a personal token, along with the database error text.

dinners.Service.Candidates now wraps every failure other than a missing
dinner in ErrCandidatesFailed, and writeDinnerError logs it as a fixed
reason (candidates_failed) with the dinner id only, answering 500
internal. Not-found, not-configured and search-failed responses are
unchanged.

Fixes MQ-98-06 (Maestro 3759a975-f937-4284-81a2-9e61345a3aa3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 00:31:08 -04:00
co-authored by Claude Opus 5.5
parent 4a209623f5
commit 9cb8812ad8
4 changed files with 78 additions and 5 deletions
+13 -3
View File
@@ -85,6 +85,11 @@ var ErrGroundTimeout = errors.New("dinners: grounding the recipe took too long")
// logged.
var ErrGroundFailed = errors.New("dinners: grounding the recipe failed")
// ErrCandidatesFailed wraps any unexpected failure finding a dinner's
// candidates. Its text may quote a candidate's URL and dependency errors,
// so it is never logged.
var ErrCandidatesFailed = errors.New("dinners: finding recipe candidates failed")
// ErrArchived is grounding an archived dinner by hand.
var ErrArchived = errors.New("dinner archived")
@@ -332,19 +337,24 @@ type offerSearch struct {
// replace the dinner's earlier candidates: one not offered again no longer
// grounds it. They ground it for offerLifetime, and not at all once it is
// grounded by hand, renamed or archived, even while this search runs. A
// search that fails wraps recipes.ErrSearchFailed.
// missing dinner is ErrNotFound; any other failure wraps
// ErrCandidatesFailed, and a search that fails also wraps
// recipes.ErrSearchFailed.
func (s *Service) Candidates(ctx context.Context, id string) ([]Candidate, error) {
// Tracked before the dinner is read: grounding, renaming or archiving
// it after the read keeps this search from storing what it found.
keep, done := s.trackOffers(id)
defer done()
row, err := s.get(ctx, id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, err
}
if err != nil {
return nil, fmt.Errorf("%w: dinner %s: %w", ErrCandidatesFailed, id, err)
}
offers, err := s.grounder.Candidates(ctx, row.Name, row.Name, MaxCandidates)
if err != nil {
return nil, err
return nil, fmt.Errorf("%w: dinner %s: %w", ErrCandidatesFailed, id, err)
}
out := make([]Candidate, len(offers))
urls := make(map[string]string, len(offers))