diff --git a/Sources/Auth/Internal/APIClient.swift b/Sources/Auth/Internal/APIClient.swift index d63547297..a08a3a10d 100644 --- a/Sources/Auth/Internal/APIClient.swift +++ b/Sources/Auth/Internal/APIClient.swift @@ -1,6 +1,10 @@ import Foundation import HTTPTypes +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + extension HTTPClient { init(configuration: AuthClient.Configuration) { var interceptors: [any HTTPClientInterceptor] = [ @@ -84,8 +88,23 @@ struct APIClient: Sendable { decoder: configuration.resolvedDecoder ) else { + // `HTTPURLResponse` does not expose the reason phrase, so Foundation's + // status description is the closest analog. The status code is always + // included because the description is localized on Darwin and differs + // from the Linux one; the empty check is defensive only. + let statusDescription = HTTPURLResponse.localizedString( + forStatusCode: response.statusCode + ) + let statusMessage = "HTTP \(response.statusCode)" + let message = + if 500..<600 ~= response.statusCode { + statusDescription.isEmpty ? statusMessage : "\(statusMessage): \(statusDescription)" + } else { + "Unexpected error" + } + return .api( - message: "Unexpected error", + message: message, errorCode: .unexpectedFailure, underlyingData: response.data, underlyingResponse: response.underlyingResponse diff --git a/Tests/AuthTests/APIClientTests.swift b/Tests/AuthTests/APIClientTests.swift new file mode 100644 index 000000000..f9ae950b6 --- /dev/null +++ b/Tests/AuthTests/APIClientTests.swift @@ -0,0 +1,110 @@ +// +// APIClientTests.swift +// Supabase +// +// Created by Muhammadjon Marufov on 02/09/26. +// + +import Foundation +import Helpers +import TestHelpers +import Testing + +@testable import Auth + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +@Suite +struct APIClientTests { + private let authClient = AuthClient( + configuration: AuthClient.Configuration( + url: URL(string: "https://project.supabase.co")!, + localStorage: InMemoryLocalStorage() + ) + ) + + private var apiClient: APIClient { + APIClient(clientID: authClient.clientID) + } + + @Test + func nonJSONServerErrorUsesStatusCodeAndDescription() async { + let data = Data("proxy failure".utf8) + let response = makeResponse(data: data, statusCode: 500) + + let error = await apiClient.handleError(response: response) + + guard + case .api( + let message, + let errorCode, + let underlyingData, + let underlyingResponse + ) = error + else { + Issue.record("Expected an API error, got \(error)") + return + } + + #expect(message == "HTTP 500: \(HTTPURLResponse.localizedString(forStatusCode: 500))") + #expect(errorCode == .unexpectedFailure) + #expect(underlyingData == data) + #expect(underlyingResponse === response.underlyingResponse) + } + + @Test + func nonJSONServerErrorWithEmptyBodyPreservesStatusCode() async { + let response = makeResponse(data: Data(), statusCode: 503) + + let error = await apiClient.handleError(response: response) + + #expect(error.message == "HTTP 503: \(HTTPURLResponse.localizedString(forStatusCode: 503))") + } + + @Test + func jsonErrorKeepsServerMessage() async { + let response = makeResponse( + data: Data(#"{"message":"Error sending confirmation email"}"#.utf8), + statusCode: 500 + ) + + let error = await apiClient.handleError(response: response) + + #expect(error.message == "Error sending confirmation email") + } + + @Test + func nonJSONServerErrorUpperBoundaryPreservesStatusCode() async { + let response = makeResponse(data: Data(), statusCode: 599) + + let error = await apiClient.handleError(response: response) + + #expect(error.message == "HTTP 599: \(HTTPURLResponse.localizedString(forStatusCode: 599))") + } + + @Test(arguments: [400, 499, 600]) + func nonJSONErrorOutsideServerRangeKeepsExistingFallback(statusCode: Int) async { + let response = makeResponse( + data: Data("bad request".utf8), + statusCode: statusCode + ) + + let error = await apiClient.handleError(response: response) + + #expect(error.message == "Unexpected error") + } + + private func makeResponse(data: Data, statusCode: Int) -> HTTPResponse { + HTTPResponse( + data: data, + response: HTTPURLResponse( + url: URL(string: "https://project.supabase.co/auth/v1/token")!, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil + )! + ) + } +} diff --git a/dictionary.txt b/dictionary.txt index 2a5d060ad..bd41ffa12 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -97,10 +97,12 @@ LinkedIn lnbm magiclink magnifyingglass +Marufov metamask metas Metas mmsdk +Muhammadjon myapp newbucket newkey