From 6bdff8f12f21cda7404a0da3f84af6160684aa91 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 24 Feb 2025 23:01:14 +0400 Subject: [PATCH 1/3] [Functions] Include endpoint and region details in error messages --- FirebaseFunctions/Sources/Functions.swift | 26 ++++++++++++------- .../Sources/FunctionsError.swift | 4 ++- .../Tests/Unit/FunctionsErrorTests.swift | 22 +++++++++++++--- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 51e405b2f39..1f2fbd58aa4 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,13 @@ 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,8 +540,8 @@ 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) // TODO: Refactor `decode(_:)` so it either returns a non-optional object or throws let payload = try serializer.decode(json) @@ -547,9 +549,15 @@ enum FunctionsConstants { return HTTPSCallableResult(data: payload as Any) } - 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..aec61168d8d 100644 --- a/FirebaseFunctions/Sources/FunctionsError.swift +++ b/FirebaseFunctions/Sources/FunctionsError.swift @@ -180,7 +180,7 @@ 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 +224,8 @@ struct FunctionsError: CustomNSError { var userInfo = [String: Any]() userInfo[NSLocalizedDescriptionKey] = description + userInfo["region"] = region + userInfo["url"] = url.absoluteString 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() ) From 8f728f591e1dba8d5f6bb344c97f26880a400d6c Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 27 Feb 2025 18:47:00 +0400 Subject: [PATCH 2/3] [Functions] Fix the code style --- FirebaseFunctions/Sources/Functions.swift | 6 ++++-- FirebaseFunctions/Sources/FunctionsError.swift | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 1f2fbd58aa4..4900866feeb 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -523,7 +523,8 @@ enum FunctionsConstants { return fetcher } - private func processedError(fromResponseError error: any Error, endpointURL url: URL) -> 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( @@ -540,7 +541,8 @@ enum FunctionsConstants { return localError ?? error } - private func callableResult(fromResponseData data: Data, endpointURL url: URL) throws -> HTTPSCallableResult { + 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) // TODO: Refactor `decode(_:)` so it either returns a non-optional object or throws diff --git a/FirebaseFunctions/Sources/FunctionsError.swift b/FirebaseFunctions/Sources/FunctionsError.swift index aec61168d8d..190503969bb 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, region: String, url: URL, 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) From f696cd29ce23d9f9bf48a40843beed8599c7ba37 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Sat, 1 Mar 2025 15:49:57 +0400 Subject: [PATCH 3/3] [Functions] Save the original URL instead of the absolute path --- FirebaseFunctions/Sources/FunctionsError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseFunctions/Sources/FunctionsError.swift b/FirebaseFunctions/Sources/FunctionsError.swift index 190503969bb..34e896b63d4 100644 --- a/FirebaseFunctions/Sources/FunctionsError.swift +++ b/FirebaseFunctions/Sources/FunctionsError.swift @@ -226,7 +226,7 @@ struct FunctionsError: CustomNSError { var userInfo = [String: Any]() userInfo[NSLocalizedDescriptionKey] = description userInfo["region"] = region - userInfo["url"] = url.absoluteString + userInfo["url"] = url if let details { userInfo[FunctionsErrorDetailsKey] = details }