Skip to content

Stop asking for a bug report when Apple is just declining - #177

Merged
parawanderer merged 4 commits into
mainfrom
fix/apple-declining-is-not-a-bug
Sep 12, 2026
Merged

parawanderer merged 4 commits into
mainfrom
fix/apple-declining-is-not-a-bug

Conversation

@parawanderer

Copy link
Copy Markdown
Owner

Fixes #176.

What the report shows

Half of it is the #168 fix working. Apple refused the 2FA submit, the exporter waited, requested a new code, and signed in:

16:29:47  Apple took the code and then failed to finish signing in: 503
16:31:02  Attempting authentication for user …
16:31:03  GSA authentication successful
16:31:03  REQUIRE_2FA -> AUTHENTICATED -> LOGGED_IN

Then open_client called request_pet one second later and got another 503, which reached the wizard's catch-all: an exception type, a log path, and a link to the issue tracker. The next attempt a minute later was refused at login and did the same. The user filed an issue because the program asked them to, about Apple having a bad three minutes.

Three call sites, one outage, one covered.

Why nothing could tell

FindMy.py folded every non-OK status into UnhandledProtocolError, whose docstring says "This is almost always a bug, so please report it". A 503 is not that: nothing is unmodelled, the server declined. Distinguishing them meant parsing the status back out of the message text.

Upstream

parawanderer/FindMy.py@3c2b492 adds AppleServiceUnavailableError, carrying the status and subclassing UnhandledProtocolError so every existing except keeps working. is_service_unavailable covers 429 and 5xx and deliberately nothing else, since a 401 is permanent until the user acts and waiting that out silently is the same mistake reversed. Raised from _gsa_request and _sms_2fa_request. 19 tests, suite green at 848.

Re-pinned in all four places per rule 14, verified by installing the new pin into a clean venv and running the bridge suite against it: 233 passed.

Here

apple_is_declining walks the cause chain, because the two uncovered sites arrive differently: log_in raises it plainly, open_client raises it several frames down and wrapped. Only the second reached the catch-all, which is why it was the one that generated the issue.

  • wizard catches the plain case by type, and the wrapped case in _report_unexpected
  • CLI catches it ahead of its UnhandledProtocolError handler
  • the message names the status, says the fault is Apple's, says the password and code are fine, says nothing was changed, and says when reporting would be reasonable rather than never

Nothing retries. The 2FA path can, because it knows what to re-do, and #176 shows it working. A refused login or request_pet has nothing to re-do but the whole sign-in.

Tests

12 here, 19 upstream. All verified failable:

Break Turns red
match by type without walking the cause chain 2
put the issue link back in the message 2
revert the GSA classification upstream 1
treat 401 as weather upstream 2

590 passed, 2 skipped across python/test and opentagviewer_export. flake8 and pyright clean.

Not verified

No Android SDK here, so the app half is CI's to run. The app reaches the same library and will now receive the new type; it has no handler for it yet, and AppleLoginActivity already has its own 2FA-specific handling from the earlier fix. Worth a follow-up rather than folding in blind.

🤖 Generated with Claude Code

Issue #176, and it is the same 503 as #168 at two call sites nothing
covered.

That log is worth reading, because half of it is the previous fix
working. Apple refused the 2FA submit; the exporter waited, requested a
new code, and signed in - GSA authentication successful, LOGGED_IN. Then
open_client called request_pet one second later, got another 503, and
that one reached the wizard's catch-all: an exception type, a log path,
and a link to the issue tracker. The next attempt a minute later was
refused at login and did the same. So the user filed an issue, because
the program asked them to, about Apple having a bad three minutes.

The obstacle was that FindMy.py folded every non-OK status into
UnhandledProtocolError, whose meaning is "Apple said something this
library does not model" - which is a bug and is worth reporting. Nothing
downstream could tell that apart from a refusal without parsing the
status back out of the message.

So the fork gained AppleServiceUnavailableError, carrying the status and
subclassing UnhandledProtocolError so existing handlers keep working, and
raises it for 429 and 5xx from both sign-in requests. FindMy.py is
re-pinned in all four places per rule 14, verified by installing the new
pin and running the bridge suite against it.

Here, apple_is_declining walks the cause chain, because the two call
sites arrive differently: log_in raises it plainly and open_client raises
it several frames down, wrapped. Only the second reached the catch-all,
which is why it was the one that produced the issue. The wizard catches
the plain case by type and the wrapped case in _report_unexpected; the
CLI catches it ahead of its UnhandledProtocolError handler.

Nothing retries. The 2FA path can, because it knows what to re-do - the
code is spent and a new one can be requested - and that is left alone
because #176 shows it working. A refused login or request_pet has nothing
to re-do but the whole sign-in, so it says what happened and stops.

Twelve tests here, nineteen in the fork. All verified failable: matching
by type without walking the chain turns two red, and putting the issue
link back in the message turns two more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The exporter half of this landed with the pin bump; the app reaches the
same library and had nothing for the new type. It also had a regression
waiting, which is the part worth reading first.

**ACodeAppleAlreadyTook.spentIt matched the string "UnhandledProtocolError"
in the bridged message.** Its own docstring says there is nothing better
to match on "until the fork grows a transient-failure type". The fork now
has one, and it is a subclass - so a real 503 arrives as
AppleServiceUnavailableError, the match fails, and the 2FA wait-and-retry
silently stops firing. Every test in that file writes the old name into
its own fixture, so all eight checks stayed green over it. A test that
supplies the string it is looking for cannot notice the string changing.
It now matches both names, and three tests cover the one that is real.

Beyond that, a 503 outside the 2FA path had no classification at all:

- classifyLoginFailure sent it to REASON_UNKNOWN, so the login screen
  echoed "The Grand Slam request was refused with HTTP 503" verbatim.
  Correct, unreadable, and indistinguishable from a bug here, which is
  how issue #176 came to be filed.
- icloud_bridge._unexpected did the same, so the fetch screen fell to its
  default branch and put the same text on screen.

Both now report apple_declined, which the Java side maps to
ICloudFailure.APPLE_DECLINED and PythonAccountLoginException's matching
reason. Deliberately not CREDENTIALS_REJECTED: that ends in a forced
sign-out, and signing somebody out over a fault that clears in minutes is
rule 15's mistake in the direction that costs a working session. Also
deliberately not REASON_NETWORK, because the advice differs - a network
failure is usually the phone's and worth checking, and this one is not.

The login screen gets a sentence saying it is Apple's fault and the
password is fine. The fetch screen reaches the retry container it already
had, with wording that says which of the two failures it is rather than
the raw detail.

Fourteen tests. Three JVM for the regression, five JVM for the mapping
and the sign-out predicate, six Python across both classifiers, and two
Espresso - one per screen - asserting the sentence is shown and that the
status code and "Grand Slam" are not.

Three strings in ten locales via add_strings.py.

Not verified locally: no Android SDK here, so the JVM and emulator suites
are CI's to run. The Python bridge suite passes at 239.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five people have now reported this and only @parawanderer has confirmed
it resolving. crishpeen on #168 says the opposite: every attempt, every
2FA method, device-identity.json cleared, still refused - and their log
shows login succeeding and only request_pet getting the 503.

The message said it "usually clears on its own within a few minutes",
which was one confirmed recovery stated as a rule. Somebody it does not
clear for would read that and wait.

It now says trying again shortly is worth doing, that it has been seen to
last, and asks for a report with the log when it keeps refusing. The 2FA
message said the same thing and said it immediately after demonstrating
that two waits had not cleared it, which was incoherent as well as
unsupported.

Both Android strings reworded through --show and --replace across ten
locales.

The test asserting no bug report is mentioned is narrowed to what it was
actually protecting: the dialog must not present this as a defect here,
which is what linking the issue tracker did. Asking for a report when it
persists is correct and now asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
parawanderer added a commit that referenced this pull request Sep 12, 2026
#182 was opened against another branch to stack it on #177, and reported one green check - the
Windows binary build, the only workflow with no branch filter. Every workflow that matters is
gated `pull_request: branches: [ "main" ]`, so the APK build, the static checks, the Chaquopy
bridge tests, the JVM suite and the emulator suite all matched nothing and never ran.

It does not present as skipped, it presents as passing, which is the part worth a rule.
#173 and #154 landed while this was open. Both sides had appended to strings.xml, in all ten
locales, at the same point in the file - so every conflict is additive and both sets are kept.
`add_strings.py --check` passes at 370 strings across 9 translated locales.
@parawanderer
parawanderer merged commit 2ce0d86 into main Sep 12, 2026
8 checks passed

This branch was successfully deployed

1 active deployment
Android Build — aebca31d Deployed Sep 12, 2026 by parawanderer via Instrumented tests (emulator) #225
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exporter: Unhandled Protocol Error: Error Response for GSA Request: 503

1 participant