From 023265c4334a3be46247436fe526ba7c6a72ca7b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 5 Sep 2026 17:02:17 -0700 Subject: [PATCH 1/2] Uses a separate connection for each GrandSlam request Apple's edge began serving at most two requests per connection around 2026-08-31, answering everything after that with a 503 HTML error page. Authenticating takes three requests -- init, complete, and apptokens -- and URLSession sends them over one pooled connection, so apptokens was rejected every time and signing in always failed at the last step. Give each request its own session, and therefore its own connection. Verified by replaying a captured request: identical bytes succeed on a fresh connection and return 503 as the third request on a reused one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WrnGBkYZDRb57McNgLxCaE --- AltSign/Sources/ALTAppleAPI+Authentication.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/AltSign/Sources/ALTAppleAPI+Authentication.swift b/AltSign/Sources/ALTAppleAPI+Authentication.swift index 2d3e50c2..4f998a15 100644 --- a/AltSign/Sources/ALTAppleAPI+Authentication.swift +++ b/AltSign/Sources/ALTAppleAPI+Authentication.swift @@ -446,7 +446,16 @@ private extension ALTAppleAPI request.httpBody = bodyData httpHeaders.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) } - let dataTask = self.session.dataTask(with: request) { (data, response, error) in + // Apple's edge serves at most two requests per connection and answers every request + // after that with a 503 HTML error page. Authenticating takes three requests (init, + // complete, apptokens), so the third one fails whenever they share a pooled connection. + // Give each request its own session, and therefore its own connection. + let configuration = URLSessionConfiguration.ephemeral + configuration.httpMaximumConnectionsPerHost = 1 + let session = URLSession(configuration: configuration) + defer { session.finishTasksAndInvalidate() } + + let dataTask = session.dataTask(with: request) { (data, response, error) in do { guard let data = data else { throw error ?? ALTAppleAPIError.unknown() } From 1427cb15b0833ad045b727a0cf5ab7b194bbc677 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 5 Sep 2026 17:02:30 -0700 Subject: [PATCH 2/2] Reports server errors instead of failing to parse them The response body is parsed as a property list before the status code is checked, so an HTML error page from Apple surfaces as NSCocoaErrorDomain 3840, "The data couldn't be read because it isn't in the correct format." That describes the parser rather than the failure, and sends users looking at their Apple ID, their anisette server, and their OS version. Check for a 5xx first and report it as what it is. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WrnGBkYZDRb57McNgLxCaE --- AltSign/Sources/ALTAppleAPI+Authentication.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AltSign/Sources/ALTAppleAPI+Authentication.swift b/AltSign/Sources/ALTAppleAPI+Authentication.swift index 4f998a15..34149216 100644 --- a/AltSign/Sources/ALTAppleAPI+Authentication.swift +++ b/AltSign/Sources/ALTAppleAPI+Authentication.swift @@ -460,6 +460,14 @@ private extension ALTAppleAPI { guard let data = data else { throw error ?? ALTAppleAPIError.unknown() } + // Surface server errors directly; their HTML bodies are not property lists, and + // parsing them yields a misleading "data couldn't be read" error instead. + if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 500 + { + let message = String(format: NSLocalizedString("Apple's authentication servers returned an error (HTTP %d). This is a problem on Apple's end, not with your Apple ID or password.", comment: ""), httpResponse.statusCode) + throw NSError(domain: ALTUnderlyingAppleAPIErrorDomain, code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: message]) + } + guard let responseDictionary = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], let dictionary = responseDictionary["Response"] as? [String: Any], let status = dictionary["Status"] as? [String: Any]