diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 0af64811e35..ce189579c87 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -401,9 +401,9 @@ enum FunctionsConstants { do { let rawData = try await fetcher.beginFetch() - return try callableResult(fromResponseData: rawData) + return try callableResult(fromResponseData: rawData, endpointURL: url) } catch { - throw processedError(fromResponseError: error) + throw processedError(fromResponseError: error, endpointURL: url) } } @@ -454,10 +454,10 @@ enum FunctionsConstants { fetcher.beginFetch { [self] data, error in let result: Result if let error { - result = .failure(processedError(fromResponseError: error)) + result = .failure(processedError(fromResponseError: error, endpointURL: url)) } else if let data { do { - result = try .success(callableResult(fromResponseData: data)) + result = try .success(callableResult(fromResponseData: data, endpointURL: url)) } catch { result = .failure(error) } @@ -523,11 +523,14 @@ enum FunctionsConstants { return fetcher } - private func processedError(fromResponseError error: any Error) -> any Error { + private func processedError(fromResponseError error: any Error, + endpointURL url: URL) -> any Error { let error = error as NSError let localError: (any Error)? = if error.domain == kGTMSessionFetcherStatusDomain { FunctionsError( httpStatusCode: error.code, + region: region, + url: url, body: error.userInfo["data"] as? Data, serializer: serializer ) @@ -538,16 +541,23 @@ enum FunctionsConstants { return localError ?? error } - private func callableResult(fromResponseData data: Data) throws -> HTTPSCallableResult { - let processedData = try processedData(fromResponseData: data) + private func callableResult(fromResponseData data: Data, + endpointURL url: URL) throws -> HTTPSCallableResult { + let processedData = try processedData(fromResponseData: data, endpointURL: url) let json = try responseDataJSON(from: processedData) let payload = try serializer.decode(json) return HTTPSCallableResult(data: payload) } - private func processedData(fromResponseData data: Data) throws -> Data { + private func processedData(fromResponseData data: Data, endpointURL url: URL) throws -> Data { // `data` might specify a custom error. If so, throw the error. - if let bodyError = FunctionsError(httpStatusCode: 200, body: data, serializer: serializer) { + if let bodyError = FunctionsError( + httpStatusCode: 200, + region: region, + url: url, + body: data, + serializer: serializer + ) { throw bodyError } diff --git a/FirebaseFunctions/Sources/FunctionsError.swift b/FirebaseFunctions/Sources/FunctionsError.swift index f8815b3ce60..34e896b63d4 100644 --- a/FirebaseFunctions/Sources/FunctionsError.swift +++ b/FirebaseFunctions/Sources/FunctionsError.swift @@ -180,7 +180,8 @@ struct FunctionsError: CustomNSError { /// } /// ``` /// - serializer: The `FunctionsSerializer` used to decode `details` in the error body. - init?(httpStatusCode: Int, body: Data?, serializer: FunctionsSerializer) { + init?(httpStatusCode: Int, region: String, url: URL, body: Data?, + serializer: FunctionsSerializer) { // Start with reasonable defaults from the status code. var code = FunctionsErrorCode(httpStatusCode: httpStatusCode) var description = Self.errorDescription(from: code) @@ -224,6 +225,8 @@ struct FunctionsError: CustomNSError { var userInfo = [String: Any]() userInfo[NSLocalizedDescriptionKey] = description + userInfo["region"] = region + userInfo["url"] = url if let details { userInfo[FunctionsErrorDetailsKey] = details } diff --git a/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift index 99b4c8334b3..5288097aeaa 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift @@ -45,6 +45,8 @@ final class FunctionsErrorTests: XCTestCase { // The error should be `nil`. let error = FunctionsError( httpStatusCode: 200, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: nil, serializer: FunctionsSerializer() ) @@ -56,6 +58,8 @@ final class FunctionsErrorTests: XCTestCase { // The error should be inferred from the HTTP status code. let error = FunctionsError( httpStatusCode: 429, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: nil, serializer: FunctionsSerializer() ) @@ -66,7 +70,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 8) XCTAssertEqual(nsError.localizedDescription, "RESOURCE EXHAUSTED") - XCTAssertEqual(nsError.userInfo.count, 1) + XCTAssertEqual(nsError.userInfo.count, 3) } func testInitWithOKStatusCodeAndIncompleteErrorBody() { @@ -75,6 +79,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 200, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -85,7 +91,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 11) XCTAssertEqual(nsError.localizedDescription, "OUT OF RANGE") - XCTAssertEqual(nsError.userInfo.count, 1) + XCTAssertEqual(nsError.userInfo.count, 3) } func testInitWithErrorStatusCodeAndErrorBody() { @@ -96,6 +102,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 499, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -106,7 +114,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 11) XCTAssertEqual(nsError.localizedDescription, "TEST_ErrorMessage") - XCTAssertEqual(nsError.userInfo.count, 2) + XCTAssertEqual(nsError.userInfo.count, 4) XCTAssertEqual(nsError.userInfo["details"] as? Int, 123) } @@ -119,6 +127,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 401, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -133,6 +143,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 403, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -143,7 +155,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 7) // `permissionDenied`, inferred from the HTTP status code XCTAssertEqual(nsError.localizedDescription, "TEST_ErrorMessage") - XCTAssertEqual(nsError.userInfo.count, 2) + XCTAssertEqual(nsError.userInfo.count, 4) XCTAssertEqual(nsError.userInfo["details"] as? NSNull, NSNull()) } @@ -155,6 +167,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 503, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() )