Separate App Store absence from transport failures - #22
Merged
Conversation
Apple answers an unknown bundle ID with HTTP 200 and an empty result set, so lib/appStore.js was synthesising an "App not found (404)" error and the refresher was parsing that number back out of its own message. A genuine HTTP 404 from the transport landed in the same bucket, where it was exempted from the consecutive-failure cap as though six apps had left the store at once. Errors now carry the status code and Retry-After from the response, and inferred absence is flagged explicitly. Absence still bypasses the cap; a real 404 counts against it. The message keeps its historic shape because routes/index.js matches on it. A 403 or 429 also no longer records a failure against the app it interrupted. That app did nothing wrong, and the increment was costing it up to 30 days of backoff for a rate limit aimed at the client. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01158qZw5HJduNcc1srUvYP3
kasnder
marked this pull request as ready for review
August 12, 2026 17:31
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.
Two fixes to how the metadata refresher classifies App Store errors. Both are independent of each other; neither changes the refresh schedule or its request volume.
Absence is not a 404
Apple answers an unknown bundle ID with HTTP 200 and an empty result set — verified against the live endpoint, for both the
bundleIdand numericidforms:bundleId=com.definitely.not.a.real.app.zzz999{"resultCount":0,"results":[]}bundleId=com.apple.PagesresultCount:1lib/appStore.jswas synthesising anApp not found (404)error for that case, andrefresh-app-store-metadata.jswas regex-matching the number back out of the message it had just written. A genuine HTTP 404 from the transport produced a different message but parsed to the same 404, so it landed in the same bucket — and was exempted from the consecutive-failure cap, as though six apps had left the store simultaneously rather than the endpoint being unreachable.Errors raised by
requestJsonnow carrystatusCodeandretryAfterfrom the response, and inferred absence is flagged withabsent: true. Absence still bypasses the transport cap; a real 404 now counts against it. TheApp not found (404)message text is unchanged becauseroutes/index.js:590matches on it.A rate limit no longer penalises the app it interrupted
On a 403 or 429 the run recorded a failure against whichever app happened to be next in the queue before aborting. That incremented
refresh_failures, which drives the exponential backoff in the selection query — costing an innocent app up to 30 days of skipped refreshes for a rate limit aimed at the client. Repeated stops walked down the queue poisoning one app each time.The stop now aborts without the write. The run-level
failedcounter still reflects it, andRetry-Afteris surfaced instoppedReasonwhen Apple sends one.Tests
node --test— 123 passing. Updated the existing 429 test, which asserted the old write, and added coverage for theRetry-Afterreason, a real 404 consuming the cap, and absence classification by both flag and legacy message shape. The newlib/appStore.jstest hits the live endpoint, consistent with the others in that file.Not included
Also discussed but deliberately left out: a canary app per run and an empty-result-rate cap, both proposed to distinguish a soft ban from a delisting. That distinction turned out not to need them — a ban arrives as 403/429 or a dropped connection, which the existing stop conditions already separate at the transport layer. The not-found semantics around regional availability are untouched;
countrystill defaults togb, so an app pulled from the UK store while alive elsewhere is still recorded asapp_not_found.Generated by Claude Code