Skip to content

[Functions] Include endpoint and region details in error messages #14487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
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,18 +541,25 @@ 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)
// TODO: Remove `as Any` once `decode(_:)` is refactored
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
}

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
Loading