Skip to content

Commit

Permalink
[Functions] Include endpoint and region details in error messages (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
ns-vasilev authored Mar 3, 2025
1 parent 0ea9a19 commit 8710c7a
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 14 deletions.
28 changes: 19 additions & 9 deletions FirebaseFunctions/Sources/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -454,10 +454,10 @@ enum FunctionsConstants {
fetcher.beginFetch { [self] data, error in
let result: Result<HTTPSCallableResult, any Error>
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)
}
Expand Down Expand Up @@ -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
)
Expand All @@ -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
}

Expand Down
5 changes: 4 additions & 1 deletion FirebaseFunctions/Sources/FunctionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
22 changes: 18 additions & 4 deletions FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
Expand All @@ -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()
)
Expand All @@ -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() {
Expand All @@ -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()
)
Expand All @@ -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() {
Expand All @@ -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()
)
Expand All @@ -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)
}

Expand All @@ -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()
)
Expand All @@ -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()
)
Expand All @@ -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())
}

Expand All @@ -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()
)
Expand Down

0 comments on commit 8710c7a

Please sign in to comment.