Repository navigation
fix(hardcover): stop asking with a rejected API token (#3101) - #3160
Merged
Merged
Conversation
A 401 from Hardcover was classified for the error message but never recorded, so every row of a bulk import sent its own doomed request. A rejected token (401, or a 403 whose body is an authentication failure) now records a hold keyed by the token fingerprint, in its own private setting with the same HMAC key as the daily quota holds. Every call through any client sharing the store, primary, enricher, cover, ISBN and list paths included, is refused without a request. The hold ends when the token changes, after one hour (one probe request, then renewed or cleared), on a successful response, or when Test connection succeeds. Test connection always sends its request so a fixed token can be retested. The new metadata.TokenRejectedError is its own type, not DailyQuotaError, and every stop point (aggregator ISBN walk, import breaker, per row reason, discovery backoff) asks metadata.IsProviderHold instead of matching one type. System status reports hardcoverTokenRejected and Settings shows a notice. Signed-off-by: vavallee <vavallee@protonmail.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
… status flags (#3101) A 401 whose body is an HTML or plain text page came from a proxy or CDN in front of Hardcover, which answers a bad token with a JSON invalid_token body. Pausing on the page would lock a valid token out for an hour, so only a JSON or empty 401 starts the hold. Cases added for a proxy page, a gateway text body and a Cloudflare challenge. GET /api/v1/system/status computed its two Hardcover fields inline in main with no test. The logic moves to hardcoverHoldStatus, which is covered for no hold, a daily pause, a rejected token (also joined with a write failure), a wrapped error and an unrelated error. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
#3101) The precedence in DailyQuota.Check had no test: making the daily hold win survived every existing test, so a rejected token over its quota would have promised a reset that cannot help. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
12 of 15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#2699 made Hardcover work stop on a daily quota hold, but a 401 never reached any of it:
classifyHTTPErrorworded the "token rejected" message andobserveonly read rate limit headers. A bulk import with a rejected token therefore still sent one doomed request per row. This records the rejection as a hold keyed by the token, so the first 401 stops every caller and nothing asks again until the token changes. Closes #3101.Design
invalid_token,unauthorized,invalid_grant, "Unable to verify token")isTokenRejection:insufficient_scope,access_deniedandinvalid_requestcan describe one query, and the refusedilikeoperator 403 and "Token bucket exhausted" never count. 5xx never counts. GraphQL errors inside a 200 are not widened in.metadata.TokenRejectedError(notDailyQuotaError),Unwraps to the classified upstream error on the first call, no reset time, never carries the tokenmetadata.IsProviderHold/ProviderHoldreplace everyerrors.As(&DailyQuotaError)WithRejectedTokenProbe(ctx)always sends its request; a success clears the holdauth.hardcover_rejected_holds(secret, hidden from the settings API), same HMAC key, same retry of a failed write before admitting anything, corrupt row logged and ignoredwriteMuEvery Hardcover call funnels through
query(), so the primary, an enricher, the cover and ISBN paths, the list syncer and the list browse all refuse fast with no per-site wiring.Aggregator.CheckQuotastays primary only: a held enricher fails its own calls without a request and the primary's answer still counts.Call sites
Matched
DailyQuotaErrorand now useIsProviderHold/ProviderHold:internal/metadata/aggregator.go(ISBN walk),internal/migrate/outage.go(import breaker),internal/migrate/authors.go(per row reason),cmd/bindery/discovery_wiring.go(discoveryBackoff).cmd/bindery/main.goreads the hold forGET /api/v1/system/statusand reportshardcoverTokenRejected.Stop on any
CheckQuotaerror, so they pick the new error up unchanged and each has a test: ABS import, scheduler refresh, CSV import, Readarr import (same walk as CSV), catalogue sync (internal/api/authors.go), list syncer (through the client).Surfacing
Settings, API Keys shows a notice while the token is rejected (
settings.general.hardcoverTokenRejected, en.json only). The import result carriesHardcover rejected the API token ...as the failure message. Docs: DEPLOYMENT.md "Hardcover rejected token pause", API.md status field.Checklist
commitsskill:docs/,README.md, godoc, Helm values)Test plan
go build ./...,go vet ./internal/... ./cmd/...golangci-lint run --concurrency 2on internal/metadata/..., internal/api, internal/migrate, internal/abs, internal/scheduler, internal/hardcoverlistsyncer, cmd/binderygo test -p 2 -count=1on the same packagesgo test -race -count=1 ./internal/metadata/hardcover/cd web && npx tsc --noEmit,npx eslinton the touched files,npx vitest run src/pages/SettingsPage.test.tsxTestCSVImportWithRejectedTokenAsksHardcoverOncesends a 10 row CSV import through a real client and counts transport requests and expects 1 request and a stopped import; with the recording removed (the pre-fix behaviour) the import runs on past the first 401 and the test failsmake smoke, the/system/statusclosure incmd/bindery/main.go(inline in main, read only)🤖 Generated with Claude Code
https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9