Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions FirebaseAuth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased
- [fixed] Fix a race condition with User.providerData getter. (#15950)
- [fixed] Fixed a release-build crash in networking code when using
Xcode 26.4 (Swift 6.3) that was caused by a Swift regression in `async let`
teardown. (#15974)

# 12.9.0
- [fixed] Stop doing unnecessary AppCheck token refreshes. Introduced
Expand Down
69 changes: 42 additions & 27 deletions FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,38 +64,53 @@ final class AuthBackend: AuthBackendProtocol {
httpMethod: String,
contentType: String,
requestConfiguration: AuthRequestConfiguration) async -> URLRequest {
// Previously, this section used `async let`, but that was changed for a
// `Task`-based approach to work around a Swift 6.3 regression in Xcode 26.4.
// - Context: https://github.com/firebase/firebase-ios-sdk/issues/15974
// Kick off tasks for the async header values.
async let heartbeatsHeaderValue = requestConfiguration.heartbeatLogger?.asyncHeaderValue()
async let appCheckTokenHeaderValue = requestConfiguration.appCheck?
.getToken(forcingRefresh: false)

var request = URLRequest(url: url)
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
let additionalFrameworkMarker = requestConfiguration.additionalFrameworkMarker
let clientVersion = "iOS/FirebaseSDK/\(FirebaseVersion())/\(additionalFrameworkMarker)"
request.setValue(clientVersion, forHTTPHeaderField: "X-Client-Version")
request.setValue(Bundle.main.bundleIdentifier, forHTTPHeaderField: "X-Ios-Bundle-Identifier")
request.setValue(requestConfiguration.appID, forHTTPHeaderField: "X-Firebase-GMPID")
request.httpMethod = httpMethod
let preferredLocalizations = Bundle.main.preferredLocalizations
if preferredLocalizations.count > 0 {
request.setValue(preferredLocalizations.first, forHTTPHeaderField: "Accept-Language")
let heartbeatsHeaderValue = Task {
await requestConfiguration.heartbeatLogger?.asyncHeaderValue()
}
if let languageCode = requestConfiguration.languageCode,
languageCode.count > 0 {
request.setValue(languageCode, forHTTPHeaderField: "X-Firebase-Locale")
let appCheckTokenHeaderValue = Task {
await requestConfiguration.appCheck?.getToken(forcingRefresh: false)
}
// Wait for the async header values.
await request.setValue(heartbeatsHeaderValue, forHTTPHeaderField: "X-Firebase-Client")
if let tokenResult = await appCheckTokenHeaderValue {
if let error = tokenResult.error {
AuthLog.logWarning(code: "I-AUT000018",
message: "Error getting App Check token; using placeholder " +
"token instead. Error: \(error)")

return await withTaskCancellationHandler {
defer {
heartbeatsHeaderValue.cancel()
appCheckTokenHeaderValue.cancel()
}
var request = URLRequest(url: url)
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
let additionalFrameworkMarker = requestConfiguration.additionalFrameworkMarker
let clientVersion = "iOS/FirebaseSDK/\(FirebaseVersion())/\(additionalFrameworkMarker)"
request.setValue(clientVersion, forHTTPHeaderField: "X-Client-Version")
request.setValue(Bundle.main.bundleIdentifier, forHTTPHeaderField: "X-Ios-Bundle-Identifier")
request.setValue(requestConfiguration.appID, forHTTPHeaderField: "X-Firebase-GMPID")
request.httpMethod = httpMethod
let preferredLocalizations = Bundle.main.preferredLocalizations
if preferredLocalizations.count > 0 {
request.setValue(preferredLocalizations.first, forHTTPHeaderField: "Accept-Language")
}
if let languageCode = requestConfiguration.languageCode,
languageCode.count > 0 {
request.setValue(languageCode, forHTTPHeaderField: "X-Firebase-Locale")
}
// Wait for the async header values.
await request.setValue(heartbeatsHeaderValue.value, forHTTPHeaderField: "X-Firebase-Client")
if let tokenResult = await appCheckTokenHeaderValue.value {
if let error = tokenResult.error {
AuthLog.logWarning(code: "I-AUT000018",
message: "Error getting App Check token; using placeholder " +
"token instead. Error: \(error)")
}
request.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
}
request.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
return request
} onCancel: {
heartbeatsHeaderValue.cancel()
appCheckTokenHeaderValue.cancel()
}
return request
}

private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? {
Expand Down
5 changes: 5 additions & 0 deletions FirebaseFunctions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Unreleased
- [fixed] Fixed a release-build crash in `HTTPSCallable.call()` when using
Xcode 26.4 (Swift 6.3) that was caused by a Swift regression in `async let`
teardown. (#15974)

# 12.0.0
- [changed] **Breaking Change**: Mark `HTTPSCallable` and `HTTPSCallableOptions`
as `final` classes for Swift clients. This was to achieve Swift 6 checked
Expand Down
36 changes: 25 additions & 11 deletions FirebaseFunctions/Sources/Internal/FunctionsContext.swift
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since authToken.value can throw, an error will cause the operation block to exit early. Unlike async let, unstructured Tasks are not automatically cancelled when they go out of scope. This could lead to appCheckToken and limitedUseAppCheckToken continuing to run in the background if authToken fails.

Adding a defer block to cancel the tasks ensures they are cleaned up regardless of whether the function succeeds or throws.

Also, a task can be canceled more than once with no ill effects.

Sources:

Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,32 @@ struct FunctionsContextProvider: Sendable {
}

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

// Only `authToken` is throwing, but the formatter script removes the `try`
// from `try authToken` and puts it in front of the initializer call.
return try await FunctionsContext(
authToken: authToken,
fcmToken: messaging?.fcmToken,
appCheckToken: appCheckToken,
limitedUseAppCheckToken: limitedUseAppCheckToken
)
return try await withTaskCancellationHandler {
defer {
authToken.cancel()
appCheckToken.cancel()
limitedUseAppCheckToken.cancel()
}
// Only `authToken` is throwing, but the formatter script removes the `try`
// from `try authToken` and puts it in front of the initializer call.
return try await FunctionsContext(
authToken: authToken.value,
fcmToken: messaging?.fcmToken,
appCheckToken: appCheckToken.value,
limitedUseAppCheckToken: limitedUseAppCheckToken.value
)
} onCancel: {
authToken.cancel()
appCheckToken.cancel()
limitedUseAppCheckToken.cancel()
}
}

private func getAppCheckToken(options: HTTPSCallableOptions?) async -> String? {
Expand Down
Loading