Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 FirebaseStorage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Unreleased
- [fixed] Fixed an issue where `putFile` could fail on the iOS Simulator with `POSIX errno 40`
(`EMSGSIZE`) due to a QUIC bug in background sessions, and improved error formatting
for `NSPOSIXErrorDomain` errors. (#16351)
- [fixed] Fixed a race condition in Storage task cancellation and improved Swift 6 strict
concurrency compliance for `StorageTask`. (#16353)
- [fixed] Fixed a crash that could occur when parsing malformed `gs://` URLs, and improved
Expand Down
5 changes: 5 additions & 0 deletions FirebaseStorage/Sources/Internal/StorageFetcherService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ actor StorageFetcherService {
if let _fetcherService {
_fetcherService.testBlock = testBlock
}
for bucketMap in fetcherServiceMap.values {
for fetcherService in bucketMap.values {
fetcherService.testBlock = testBlock
}
}
}

private var testBlock: GTMSessionFetcherTestBlock?
Expand Down
20 changes: 17 additions & 3 deletions FirebaseStorage/Sources/StorageError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,23 @@ public let StorageErrorDomain: String = "FIRStorageErrorDomain"
case 404: StorageError.objectNotFound(
object: ref.path.object ?? "<object-entity-internal-error>", serverError: errorDictionary
)
default: StorageError.unknown(
message: "Unexpected \(serverError.code) code from backend", serverError: errorDictionary
)
default:
{ () -> StorageError in
if serverError.domain == NSPOSIXErrorDomain {
let errorCode = Int32(truncatingIfNeeded: serverError.code)
var buffer = [CChar](repeating: 0, count: 128)
_ = strerror_r(errorCode, &buffer, buffer.count)
return StorageError.unknown(
message: "POSIX errno \(serverError.code) (\(String(cString: buffer)))",
serverError: errorDictionary
)
} else {
return StorageError.unknown(
message: "Unexpected \(serverError.code) code from backend",
serverError: errorDictionary
)
}
}()
Comment thread
paulb777 marked this conversation as resolved.
Outdated
Comment thread
ncooke3 marked this conversation as resolved.
Outdated
}
return storageError as NSError
}
Expand Down
8 changes: 6 additions & 2 deletions FirebaseStorage/Sources/StorageUploadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,13 @@ import Foundation
uploadFetcher.uploadFileURL = fileURL
uploadFetcher.comment = "File UploadTask"

if !GULAppEnvironmentUtil.supportsBackgroundURLSessionUploads() {
#if targetEnvironment(simulator)
uploadFetcher.useBackgroundSession = false
}
#else
if !GULAppEnvironmentUtil.supportsBackgroundURLSessionUploads() {
uploadFetcher.useBackgroundSession = false
}
#endif
Comment thread
paulb777 marked this conversation as resolved.
Comment thread
paulb777 marked this conversation as resolved.
}
uploadFetcher.maxRetryInterval = self.reference.storage.maxUploadRetryInterval

Expand Down
40 changes: 40 additions & 0 deletions FirebaseStorage/Tests/Integration/StoragePOSIXErrorTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

@testable import FirebaseStorage
import Foundation
#if COCOAPODS
import GTMSessionFetcher
#else
import GTMSessionFetcherCore
#endif
import XCTest

@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *)
class StoragePOSIXErrorTest: StorageIntegrationCommon {
func testPutFileWithPOSIXError40() async throws {
let ref = storage.reference(withPath: "ios/public/testPOSIX40")

let data = try XCTUnwrap("Hello".data(using: .utf8))
let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory())
let fileURL = tmpDirURL.appendingPathComponent(#function + "hello.txt")
try data.write(to: fileURL, options: .atomicWrite)
Comment thread
paulb777 marked this conversation as resolved.
defer {
Comment thread
ncooke3 marked this conversation as resolved.
Outdated
try? FileManager.default.removeItem(at: fileURL)
}

let metadata = try await ref.putFileAsync(from: fileURL)
XCTAssertEqual(metadata.size, Int64(data.count))
}
}
Comment thread
paulb777 marked this conversation as resolved.
59 changes: 59 additions & 0 deletions FirebaseStorage/Tests/Unit/StoragePutFileTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

@testable import FirebaseStorage
import Foundation
#if COCOAPODS
import GTMSessionFetcher
#else
import GTMSessionFetcherCore
#endif
import XCTest

class StoragePutFileTests: StorageTestHelpers {
func testPutFileWithPOSIXError40UpdatesExistingFetcherService() async throws {
// Initialize storage *before* setting the testBlock to verify that
// `StorageFetcherService.updateTestBlock` correctly updates pre-existing instances.
let storageInstance = storage()
let ref = storageInstance.reference(withPath: "ios/public/testPOSIX40")

let testBlock: GTMSessionFetcherTestBlock = { fetcher, response in
let error = NSError(domain: NSPOSIXErrorDomain, code: 40, userInfo: nil)
response(nil, nil, error)
}
await StorageFetcherService.shared.updateTestBlock(testBlock)
addTeardownBlock {
await StorageFetcherService.shared.updateTestBlock(nil)
}

let data = try XCTUnwrap("Hello".data(using: .utf8))
let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory())
let fileURL = tmpDirURL.appendingPathComponent(#function + "hello.txt")
try data.write(to: fileURL, options: .atomicWrite)
defer {
try? FileManager.default.removeItem(at: fileURL)
}

do {
_ = try await ref.putFileAsync(from: fileURL)
XCTFail("Unexpected success")
} catch let error as NSError {
XCTAssertEqual(error.domain, StorageErrorDomain)
XCTAssertEqual(error.code, StorageErrorCode.unknown.rawValue)
let message = error.localizedDescription
XCTAssertTrue(message.contains("POSIX errno 40 (Message too long)"),
"Error message should contain 'POSIX errno 40 (Message too long)', but got \(message)")
}
}
}
14 changes: 14 additions & 0 deletions FirebaseStorage/Tests/Unit/StorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,20 @@ class StorageTests: XCTestCase {
XCTAssertEqual(storage.maxOperationRetryInterval, 8)
}

func testPOSIXErrorFormatting() throws {
let app = try getApp(bucket: "bucket")
let storage = Storage.storage(app: app)
let ref = storage.reference(withPath: "ios/public/testPOSIX40")
let posixError = NSError(domain: NSPOSIXErrorDomain, code: 40, userInfo: nil)
let storageError = StorageErrorCode.error(withServerError: posixError, ref: ref)
XCTAssertEqual(storageError.domain, StorageErrorDomain)
XCTAssertEqual(storageError.code, StorageErrorCode.unknown.rawValue)
XCTAssertEqual(
storageError.userInfo[NSLocalizedDescriptionKey] as? String,
"POSIX errno 40 (Message too long)"
)
}

// MARK: Private Helpers

// Cache the app associated with each Storage bucket
Expand Down
Loading