Skip to content

Commit 9f68d4f

Browse files
authored
Fix Xcode 16 continuation crashes (#13521)
1 parent b761b15 commit 9f68d4f

File tree

11 files changed

+522
-663
lines changed

11 files changed

+522
-663
lines changed

FirebaseAuth/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 11.2.0
2+
- [Fixed] Fixed crashes that could occur in Swift continuation blocks running in the Xcode 16
3+
betas. (#13480)
4+
15
# 11.1.0
26
- [fixed] Fixed `Swift.error` conformance for `AuthErrorCode`. (#13430)
37
- [added] Added custom provider support to `AuthProviderID`. Note that this change will be breaking

FirebaseAuth/Docs/threading.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ has its target queue set to this auth global work queue. This way we don't
2222
have to think about which variables may be contested. We only need to make
2323
sure all public APIs that may have thread-safety issues make the dispatch.
2424
The auth global work queue is defined in
25-
[FIRAuthGlobalWorkQueue.h](../Source/Private/FIRAuthGlobalWorkQueue.h)
26-
and any serial task queue created by
27-
[FIRAuthSerialTaskQueue.h](../Source/Private/FIRAuthSerialTaskQueue.h)
28-
already has its target set properly.
25+
[FIRAuthGlobalWorkQueue.h](../Source/Private/FIRAuthGlobalWorkQueue.h).
2926

3027
In following sub-sections, we divided methods into three categories, according
3128
to the two criteria below:

FirebaseAuth/Sources/Swift/Auth/AuthSerialTaskQueue.swift

Lines changed: 0 additions & 39 deletions
This file was deleted.

FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ import Foundation
201201
}
202202

203203
/// Starts the flow to verify the client via silent push notification.
204-
/// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an
204+
/// - Parameter retryOnInvalidAppCredential: Whether or not the flow should be retried if an
205205
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
206206
/// - Parameter phoneNumber: The phone number to be verified.
207207
/// - Parameter callback: The callback to be invoked on the global work queue when the flow is

FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

Lines changed: 114 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ protocol AuthBackendRPCIssuer: NSObjectProtocol {
3131
/// on the auth global work queue in the future.
3232
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
3333
body: Data?,
34-
contentType: String,
35-
completionHandler: @escaping ((Data?, Error?) -> Void))
34+
contentType: String) async -> (Data?, Error?)
3635
}
3736

3837
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
@@ -51,20 +50,22 @@ class AuthBackendRPCIssuerImplementation: NSObject, AuthBackendRPCIssuer {
5150

5251
func asyncCallToURL<T: AuthRPCRequest>(with request: T,
5352
body: Data?,
54-
contentType: String,
55-
completionHandler: @escaping ((Data?, Error?)
56-
-> Void)) {
53+
contentType: String) async -> (Data?, Error?) {
5754
let requestConfiguration = request.requestConfiguration()
58-
AuthBackend.request(withURL: request.requestURL(),
59-
contentType: contentType,
60-
requestConfiguration: requestConfiguration) { request in
61-
let fetcher = self.fetcherService.fetcher(with: request)
62-
if let _ = requestConfiguration.emulatorHostAndPort {
63-
fetcher.allowLocalhostRequest = true
64-
fetcher.allowedInsecureSchemes = ["http"]
55+
let request = await AuthBackend.request(withURL: request.requestURL(),
56+
contentType: contentType,
57+
requestConfiguration: requestConfiguration)
58+
let fetcher = fetcherService.fetcher(with: request)
59+
if let _ = requestConfiguration.emulatorHostAndPort {
60+
fetcher.allowLocalhostRequest = true
61+
fetcher.allowedInsecureSchemes = ["http"]
62+
}
63+
fetcher.bodyData = body
64+
65+
return await withUnsafeContinuation { continuation in
66+
fetcher.beginFetch { data, error in
67+
continuation.resume(returning: (data, error))
6568
}
66-
fetcher.bodyData = body
67-
fetcher.beginFetch(completionHandler: completionHandler)
6869
}
6970
}
7071
}
@@ -98,8 +99,7 @@ class AuthBackend: NSObject {
9899

99100
class func request(withURL url: URL,
100101
contentType: String,
101-
requestConfiguration: AuthRequestConfiguration,
102-
completion: @escaping (URLRequest) -> Void) {
102+
requestConfiguration: AuthRequestConfiguration) async -> URLRequest {
103103
var request = URLRequest(url: url)
104104
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
105105
let additionalFrameworkMarker = requestConfiguration
@@ -121,18 +121,15 @@ class AuthBackend: NSObject {
121121
request.setValue(languageCode, forHTTPHeaderField: "X-Firebase-Locale")
122122
}
123123
if let appCheck = requestConfiguration.appCheck {
124-
appCheck.getToken(forcingRefresh: false) { tokenResult in
125-
if let error = tokenResult.error {
126-
AuthLog.logWarning(code: "I-AUT000018",
127-
message: "Error getting App Check token; using placeholder " +
128-
"token instead. Error: \(error)")
129-
}
130-
request.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
131-
completion(request)
124+
let tokenResult = await appCheck.getToken(forcingRefresh: false)
125+
if let error = tokenResult.error {
126+
AuthLog.logWarning(code: "I-AUT000018",
127+
message: "Error getting App Check token; using placeholder " +
128+
"token instead. Error: \(error)")
132129
}
133-
} else {
134-
completion(request)
130+
request.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
135131
}
132+
return request
136133
}
137134
}
138135

@@ -270,135 +267,104 @@ private class AuthBackendRPCImplementation: NSObject, AuthBackendImplementation
270267
throw AuthErrorUtils.JSONSerializationErrorForUnencodableType()
271268
}
272269
}
273-
return try await withCheckedThrowingContinuation { continuation in
274-
rpcIssuer
275-
.asyncCallToURL(with: request, body: bodyData, contentType: "application/json") {
276-
data, error in
277-
// If there is an error with no body data at all, then this must be a
278-
// network error.
279-
guard let data = data else {
280-
if let error = error {
281-
continuation.resume(throwing: AuthErrorUtils.networkError(underlyingError: error))
282-
return
283-
} else {
284-
// TODO: this was ignored before
285-
fatalError("Auth Internal error: RPC call didn't return data or an error.")
286-
}
287-
}
288-
// Try to decode the HTTP response data which may contain either a
289-
// successful response or error message.
290-
var dictionary: [String: AnyHashable]
291-
do {
292-
let rawDecode = try JSONSerialization.jsonObject(with: data,
293-
options: JSONSerialization
294-
.ReadingOptions
295-
.mutableLeaves)
296-
guard let decodedDictionary = rawDecode as? [String: AnyHashable] else {
297-
if error != nil {
298-
continuation.resume(
299-
throwing: AuthErrorUtils.unexpectedErrorResponse(deserializedResponse: rawDecode,
300-
underlyingError: error)
301-
)
302-
return
303-
} else {
304-
continuation.resume(
305-
throwing: AuthErrorUtils.unexpectedResponse(deserializedResponse: rawDecode)
306-
)
307-
return
308-
}
309-
}
310-
dictionary = decodedDictionary
311-
} catch let jsonError {
312-
if error != nil {
313-
// We have an error, but we couldn't decode the body, so we have no
314-
// additional information other than the raw response and the
315-
// original NSError (the jsonError is inferred by the error code
316-
// (AuthErrorCodeUnexpectedHTTPResponse, and is irrelevant.)
317-
continuation.resume(
318-
throwing: AuthErrorUtils.unexpectedErrorResponse(
319-
data: data,
320-
underlyingError: error
321-
)
322-
)
323-
return
324-
} else {
325-
// This is supposed to be a "successful" response, but we couldn't
326-
// deserialize the body.
327-
continuation.resume(
328-
throwing: AuthErrorUtils.unexpectedResponse(data: data, underlyingError: jsonError)
329-
)
330-
return
331-
}
332-
}
270+
let (data, error) = await rpcIssuer
271+
.asyncCallToURL(with: request, body: bodyData, contentType: "application/json")
272+
// If there is an error with no body data at all, then this must be a
273+
// network error.
274+
guard let data = data else {
275+
if let error = error {
276+
throw AuthErrorUtils.networkError(underlyingError: error)
277+
} else {
278+
// TODO: this was ignored before
279+
fatalError("Auth Internal error: RPC call didn't return data or an error.")
280+
}
281+
}
282+
// Try to decode the HTTP response data which may contain either a
283+
// successful response or error message.
284+
var dictionary: [String: AnyHashable]
285+
var rawDecode: Any
286+
do {
287+
rawDecode = try JSONSerialization.jsonObject(
288+
with: data, options: JSONSerialization.ReadingOptions.mutableLeaves
289+
)
290+
} catch let jsonError {
291+
if error != nil {
292+
// We have an error, but we couldn't decode the body, so we have no
293+
// additional information other than the raw response and the
294+
// original NSError (the jsonError is inferred by the error code
295+
// (AuthErrorCodeUnexpectedHTTPResponse, and is irrelevant.)
296+
throw AuthErrorUtils.unexpectedErrorResponse(data: data, underlyingError: error)
297+
} else {
298+
// This is supposed to be a "successful" response, but we couldn't
299+
// deserialize the body.
300+
throw AuthErrorUtils.unexpectedResponse(data: data, underlyingError: jsonError)
301+
}
302+
}
303+
guard let decodedDictionary = rawDecode as? [String: AnyHashable] else {
304+
if error != nil {
305+
throw AuthErrorUtils.unexpectedErrorResponse(deserializedResponse: rawDecode,
306+
underlyingError: error)
307+
} else {
308+
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: rawDecode)
309+
}
310+
}
311+
dictionary = decodedDictionary
333312

334-
let response = T.Response()
313+
let response = T.Response()
335314

336-
// At this point we either have an error with successfully decoded
337-
// details in the body, or we have a response which must pass further
338-
// validation before we know it's truly successful. We deal with the
339-
// case where we have an error with successfully decoded error details
340-
// first:
341-
if error != nil {
342-
if let errorDictionary = dictionary["error"] as? [String: AnyHashable] {
343-
if let errorMessage = errorDictionary["message"] as? String {
344-
if let clientError = AuthBackendRPCImplementation.clientError(
345-
withServerErrorMessage: errorMessage,
346-
errorDictionary: errorDictionary,
347-
response: response,
348-
error: error
349-
) {
350-
continuation.resume(throwing: clientError)
351-
return
352-
}
353-
}
354-
// Not a message we know, return the message directly.
355-
continuation.resume(
356-
throwing: AuthErrorUtils.unexpectedErrorResponse(
357-
deserializedResponse: errorDictionary,
358-
underlyingError: error
359-
)
360-
)
361-
return
362-
}
363-
// No error message at all, return the decoded response.
364-
continuation.resume(
365-
throwing: AuthErrorUtils
366-
.unexpectedErrorResponse(deserializedResponse: dictionary, underlyingError: error)
367-
)
368-
return
315+
// At this point we either have an error with successfully decoded
316+
// details in the body, or we have a response which must pass further
317+
// validation before we know it's truly successful. We deal with the
318+
// case where we have an error with successfully decoded error details
319+
// first:
320+
if error != nil {
321+
if let errorDictionary = dictionary["error"] as? [String: AnyHashable] {
322+
if let errorMessage = errorDictionary["message"] as? String {
323+
if let clientError = AuthBackendRPCImplementation.clientError(
324+
withServerErrorMessage: errorMessage,
325+
errorDictionary: errorDictionary,
326+
response: response,
327+
error: error
328+
) {
329+
throw clientError
369330
}
331+
}
332+
// Not a message we know, return the message directly.
333+
throw AuthErrorUtils.unexpectedErrorResponse(
334+
deserializedResponse: errorDictionary,
335+
underlyingError: error
336+
)
337+
}
338+
// No error message at all, return the decoded response.
339+
throw AuthErrorUtils
340+
.unexpectedErrorResponse(deserializedResponse: dictionary, underlyingError: error)
341+
}
370342

371-
// Finally, we try to populate the response object with the JSON values.
372-
do {
373-
try response.setFields(dictionary: dictionary)
374-
} catch {
375-
continuation.resume(
376-
throwing: AuthErrorUtils
377-
.RPCResponseDecodingError(deserializedResponse: dictionary, underlyingError: error)
378-
)
379-
return
380-
}
381-
// In case returnIDPCredential of a verifyAssertion request is set to
382-
// @YES, the server may return a 200 with a response that may contain a
383-
// server error.
384-
if let verifyAssertionRequest = request as? VerifyAssertionRequest {
385-
if verifyAssertionRequest.returnIDPCredential {
386-
if let errorMessage = dictionary["errorMessage"] as? String {
387-
if let clientError = AuthBackendRPCImplementation.clientError(
388-
withServerErrorMessage: errorMessage,
389-
errorDictionary: dictionary,
390-
response: response,
391-
error: error
392-
) {
393-
continuation.resume(throwing: clientError)
394-
return
395-
}
396-
}
397-
}
343+
// Finally, we try to populate the response object with the JSON values.
344+
do {
345+
try response.setFields(dictionary: dictionary)
346+
} catch {
347+
throw AuthErrorUtils
348+
.RPCResponseDecodingError(deserializedResponse: dictionary, underlyingError: error)
349+
}
350+
// In case returnIDPCredential of a verifyAssertion request is set to
351+
// @YES, the server may return a 200 with a response that may contain a
352+
// server error.
353+
if let verifyAssertionRequest = request as? VerifyAssertionRequest {
354+
if verifyAssertionRequest.returnIDPCredential {
355+
if let errorMessage = dictionary["errorMessage"] as? String {
356+
if let clientError = AuthBackendRPCImplementation.clientError(
357+
withServerErrorMessage: errorMessage,
358+
errorDictionary: dictionary,
359+
response: response,
360+
error: error
361+
) {
362+
throw clientError
398363
}
399-
continuation.resume(returning: response)
400364
}
365+
}
401366
}
367+
return response
402368
}
403369

404370
private class func clientError(withServerErrorMessage serverErrorMessage: String,

0 commit comments

Comments
 (0)