Skip to content

Commit db5828b

Browse files
fix(auth, functions): Xcode 26.4 'async let' cleanup crashes (#15991)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent dbc9149 commit db5828b

File tree

4 files changed

+75
-38
lines changed

4 files changed

+75
-38
lines changed

FirebaseAuth/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
`Auth.idTokenChanges`, providing a modern, structured-concurrency
44
alternative to `addStateDidChangeListener` and `addIDTokenDidChangeListener`.
55
- [fixed] Fix a race condition with User.providerData getter. (#15950)
6+
- [fixed] Fixed a release-build crash in networking code when using
7+
Xcode 26.4 (Swift 6.3) that was caused by a Swift regression in `async let`
8+
teardown. (#15974)
69

710
# 12.9.0
811
- [fixed] Stop doing unnecessary AppCheck token refreshes. Introduced

FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -64,38 +64,53 @@ final class AuthBackend: AuthBackendProtocol {
6464
httpMethod: String,
6565
contentType: String,
6666
requestConfiguration: AuthRequestConfiguration) async -> URLRequest {
67+
// Previously, this section used `async let`, but that was changed for a
68+
// `Task`-based approach to work around a Swift 6.3 regression in Xcode 26.4.
69+
// - Context: https://github.com/firebase/firebase-ios-sdk/issues/15974
6770
// Kick off tasks for the async header values.
68-
async let heartbeatsHeaderValue = requestConfiguration.heartbeatLogger?.asyncHeaderValue()
69-
async let appCheckTokenHeaderValue = requestConfiguration.appCheck?
70-
.getToken(forcingRefresh: false)
71-
72-
var request = URLRequest(url: url)
73-
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
74-
let additionalFrameworkMarker = requestConfiguration.additionalFrameworkMarker
75-
let clientVersion = "iOS/FirebaseSDK/\(FirebaseVersion())/\(additionalFrameworkMarker)"
76-
request.setValue(clientVersion, forHTTPHeaderField: "X-Client-Version")
77-
request.setValue(Bundle.main.bundleIdentifier, forHTTPHeaderField: "X-Ios-Bundle-Identifier")
78-
request.setValue(requestConfiguration.appID, forHTTPHeaderField: "X-Firebase-GMPID")
79-
request.httpMethod = httpMethod
80-
let preferredLocalizations = Bundle.main.preferredLocalizations
81-
if preferredLocalizations.count > 0 {
82-
request.setValue(preferredLocalizations.first, forHTTPHeaderField: "Accept-Language")
71+
let heartbeatsHeaderValue = Task {
72+
await requestConfiguration.heartbeatLogger?.asyncHeaderValue()
8373
}
84-
if let languageCode = requestConfiguration.languageCode,
85-
languageCode.count > 0 {
86-
request.setValue(languageCode, forHTTPHeaderField: "X-Firebase-Locale")
74+
let appCheckTokenHeaderValue = Task {
75+
await requestConfiguration.appCheck?.getToken(forcingRefresh: false)
8776
}
88-
// Wait for the async header values.
89-
await request.setValue(heartbeatsHeaderValue, forHTTPHeaderField: "X-Firebase-Client")
90-
if let tokenResult = await appCheckTokenHeaderValue {
91-
if let error = tokenResult.error {
92-
AuthLog.logWarning(code: "I-AUT000018",
93-
message: "Error getting App Check token; using placeholder " +
94-
"token instead. Error: \(error)")
77+
78+
return await withTaskCancellationHandler {
79+
defer {
80+
heartbeatsHeaderValue.cancel()
81+
appCheckTokenHeaderValue.cancel()
82+
}
83+
var request = URLRequest(url: url)
84+
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
85+
let additionalFrameworkMarker = requestConfiguration.additionalFrameworkMarker
86+
let clientVersion = "iOS/FirebaseSDK/\(FirebaseVersion())/\(additionalFrameworkMarker)"
87+
request.setValue(clientVersion, forHTTPHeaderField: "X-Client-Version")
88+
request.setValue(Bundle.main.bundleIdentifier, forHTTPHeaderField: "X-Ios-Bundle-Identifier")
89+
request.setValue(requestConfiguration.appID, forHTTPHeaderField: "X-Firebase-GMPID")
90+
request.httpMethod = httpMethod
91+
let preferredLocalizations = Bundle.main.preferredLocalizations
92+
if preferredLocalizations.count > 0 {
93+
request.setValue(preferredLocalizations.first, forHTTPHeaderField: "Accept-Language")
94+
}
95+
if let languageCode = requestConfiguration.languageCode,
96+
languageCode.count > 0 {
97+
request.setValue(languageCode, forHTTPHeaderField: "X-Firebase-Locale")
98+
}
99+
// Wait for the async header values.
100+
await request.setValue(heartbeatsHeaderValue.value, forHTTPHeaderField: "X-Firebase-Client")
101+
if let tokenResult = await appCheckTokenHeaderValue.value {
102+
if let error = tokenResult.error {
103+
AuthLog.logWarning(code: "I-AUT000018",
104+
message: "Error getting App Check token; using placeholder " +
105+
"token instead. Error: \(error)")
106+
}
107+
request.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
95108
}
96-
request.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
109+
return request
110+
} onCancel: {
111+
heartbeatsHeaderValue.cancel()
112+
appCheckTokenHeaderValue.cancel()
97113
}
98-
return request
99114
}
100115

101116
private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? {

FirebaseFunctions/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Unreleased
2+
- [fixed] Fixed a release-build crash in `HTTPSCallable.call()` when using
3+
Xcode 26.4 (Swift 6.3) that was caused by a Swift regression in `async let`
4+
teardown. (#15974)
5+
16
# 12.0.0
27
- [changed] **Breaking Change**: Mark `HTTPSCallable` and `HTTPSCallableOptions`
38
as `final` classes for Swift clients. This was to achieve Swift 6 checked

FirebaseFunctions/Sources/Internal/FunctionsContext.swift

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,32 @@ struct FunctionsContextProvider: Sendable {
3737
}
3838

3939
func context(options: HTTPSCallableOptions?) async throws -> FunctionsContext {
40-
async let authToken = auth?.getToken(forcingRefresh: false)
41-
async let appCheckToken = getAppCheckToken(options: options)
42-
async let limitedUseAppCheckToken = getLimitedUseAppCheckToken(options: options)
40+
// Previously, this section used `async let`, but that was changed for a
41+
// `Task`-based approach to work around a Swift 6.3 regression in Xcode 26.4.
42+
// - Context: https://github.com/firebase/firebase-ios-sdk/issues/15974
43+
let authToken = Task { try await auth?.getToken(forcingRefresh: false) }
44+
let appCheckToken = Task { await getAppCheckToken(options: options) }
45+
let limitedUseAppCheckToken = Task { await getLimitedUseAppCheckToken(options: options) }
4346

44-
// Only `authToken` is throwing, but the formatter script removes the `try`
45-
// from `try authToken` and puts it in front of the initializer call.
46-
return try await FunctionsContext(
47-
authToken: authToken,
48-
fcmToken: messaging?.fcmToken,
49-
appCheckToken: appCheckToken,
50-
limitedUseAppCheckToken: limitedUseAppCheckToken
51-
)
47+
return try await withTaskCancellationHandler {
48+
defer {
49+
authToken.cancel()
50+
appCheckToken.cancel()
51+
limitedUseAppCheckToken.cancel()
52+
}
53+
// Only `authToken` is throwing, but the formatter script removes the `try`
54+
// from `try authToken` and puts it in front of the initializer call.
55+
return try await FunctionsContext(
56+
authToken: authToken.value,
57+
fcmToken: messaging?.fcmToken,
58+
appCheckToken: appCheckToken.value,
59+
limitedUseAppCheckToken: limitedUseAppCheckToken.value
60+
)
61+
} onCancel: {
62+
authToken.cancel()
63+
appCheckToken.cancel()
64+
limitedUseAppCheckToken.cancel()
65+
}
5266
}
5367

5468
private func getAppCheckToken(options: HTTPSCallableOptions?) async -> String? {

0 commit comments

Comments
 (0)