diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index 984b5e762..b64fc2280 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -1116,12 +1116,25 @@ - (ARTStatsResourceCount *)statsResourceCountFromDictionary:(NSDictionary *)inpu refused:refused.doubleValue]; } -- (ARTErrorInfo *)decodeErrorInfo:(NSData *)artError error:(NSError **)error { - NSDictionary *decodedError = [[self decodeDictionary:artError error:error] valueForKey:@"error"]; - if (!decodedError) { - return nil; +- (ARTErrorInfo *)decodeErrorInfo:(NSData *)artError statusCode:(NSInteger)statusCode error:(NSError **)error { + NSDictionary *dict = [self decodeDictionary:artError error:error]; + id decodedError = [dict valueForKey:@"error"]; + + if ([decodedError isKindOfClass:NSDictionary.class]) { + NSDictionary *errorDict = decodedError; + NSNumber *codeNumber = [errorDict artNumber:@"code"]; + NSNumber *statusNumber = [errorDict artNumber:@"statusCode"]; + NSString *message = [errorDict artString:@"message"]; + return [ARTErrorInfo createWithCode:[(codeNumber ?: @(statusCode * 100)) intValue] + status:[(statusNumber ?: @(statusCode)) intValue] + message:message ?: [NSString stringWithFormat:@"HTTP request failed with status code %ld", statusCode]]; + } else { + // We expect `decodedError` as a dictionary from Ably REST API, but in case user sets custom authUrl in the auth options, it can be anything + // We'll address this in an upcoming proper fix for https://github.com/ably/ably-cocoa/issues/2135 + return [ARTErrorInfo createWithCode:statusCode * 100 + status:statusCode + message:[NSString stringWithFormat:@"HTTP request failed with status code %ld", statusCode]]; } - return [ARTErrorInfo createWithCode:[decodedError[@"code"] intValue] status:[decodedError[@"statusCode"] intValue] message:decodedError[@"message"]]; } - (ARTStatsRequestCount *)statsRequestCountFromDictionary:(NSDictionary *)input { diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 4d0f41f9b..b17792a3e 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -402,7 +402,9 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary= 400) { if (data) { NSError *decodeError = nil; - ARTErrorInfo *dataError = [self->_encoders[response.MIMEType] decodeErrorInfo:data error:&decodeError]; + ARTErrorInfo *dataError = [self->_encoders[response.MIMEType] decodeErrorInfo:data + statusCode:response.statusCode + error:&decodeError]; if ([self shouldRenewToken:&dataError] && [request isKindOfClass:[NSMutableURLRequest class]]) { ARTLogDebug(self.logger, @"RS:%p retry request %@", self, request); // Make a single attempt to reissue the token and resend the request @@ -429,11 +433,10 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary= 400) { - ARTErrorInfo *dataError = [self->_encoders[response.MIMEType] decodeErrorInfo:data error:&decodeError]; + ARTErrorInfo *dataError = [self->_encoders[response.MIMEType] decodeErrorInfo:data + statusCode:response.statusCode + error:&decodeError]; callback(nil, dataError ? dataError : decodeError); } else { NSDate *time = [self->_encoders[response.MIMEType] decodeTime:data error:&decodeError]; diff --git a/Source/PrivateHeaders/Ably/ARTEncoder.h b/Source/PrivateHeaders/Ably/ARTEncoder.h index b8917750a..8295c851b 100644 --- a/Source/PrivateHeaders/Ably/ARTEncoder.h +++ b/Source/PrivateHeaders/Ably/ARTEncoder.h @@ -97,7 +97,7 @@ NS_ASSUME_NONNULL_BEGIN // Others - (nullable NSDate *)decodeTime:(NSData *)data error:(NSError *_Nullable *_Nullable)error; -- (nullable ARTErrorInfo *)decodeErrorInfo:(NSData *)error error:(NSError *_Nullable *_Nullable)error; +- (nullable ARTErrorInfo *)decodeErrorInfo:(NSData *)error statusCode:(NSInteger)statusCode error:(NSError *_Nullable *_Nullable)error; - (nullable NSArray *)decodeStats:(NSData *)data error:(NSError *_Nullable *_Nullable)error; @end diff --git a/Test/AblyTests/Test Utilities/TestUtilities.swift b/Test/AblyTests/Test Utilities/TestUtilities.swift index a575bd878..ae8217c51 100644 --- a/Test/AblyTests/Test Utilities/TestUtilities.swift +++ b/Test/AblyTests/Test Utilities/TestUtilities.swift @@ -886,8 +886,37 @@ struct ErrorSimulator { let serverId = "server-test-suite" var statusCode: Int = 401 var shouldPerformRequest: Bool = false - - mutating func stubResponse(_ url: URL) -> HTTPURLResponse? { + let stubData: Data? + + init(value: Int, description: String, statusCode: Int, shouldPerformRequest: Bool, stubData: Data?) { + self.value = value + self.description = description + self.statusCode = statusCode + self.shouldPerformRequest = shouldPerformRequest + self.stubData = stubData + } + + init(value: Int, description: String, statusCode: Int, shouldPerformRequest: Bool, stubDataDict: [String: Any]? = nil) { + self.value = value + self.description = description + self.statusCode = statusCode + self.shouldPerformRequest = shouldPerformRequest + if let stubDataDict { + self.stubData = stubDataDict.data() + } else { + let jsonObject: [String: Any] = [ + "error": [ + "statusCode": self.statusCode, + "code": self.value, + "message": self.description, + "serverId": self.serverId, + ] + ] + self.stubData = jsonObject.data() + } + } + + func stubResponse(_ url: URL) -> HTTPURLResponse? { return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: [ "Content-Length": String(stubData?.count ?? 0), "Content-Type": "application/json", @@ -897,17 +926,12 @@ struct ErrorSimulator { ] ) } +} - lazy var stubData: Data? = { - let jsonObject: [String: Any] = ["error": [ - "statusCode": modf(Float(self.value)/100).0, //whole number part - "code": self.value, - "message": self.description, - "serverId": self.serverId, - ] as [String: Any] - ] - return try? JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions.init(rawValue: 0)) - }() +extension [String: Any] { + func data() -> Data? { + try? JSONSerialization.data(withJSONObject: self, options: JSONSerialization.WritingOptions.init(rawValue: 0)) + } } class MockHTTPExecutor: NSObject, ARTHTTPExecutor { @@ -1015,7 +1039,7 @@ class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor { } } - if var simulatedError = errorSimulator, let requestURL = request.url { + if let simulatedError = errorSimulator, let requestURL = request.url { defer { errorSimulator = nil } @@ -1058,9 +1082,9 @@ class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor { return task } - func simulateIncomingServerErrorOnNextRequest(_ errorValue: Int, description: String) { + func simulateIncomingServerErrorOnNextRequest(_ errorValue: Int, statusCode: Int = 401, description: String, data: [String: Any]? = nil) { http.queue.sync { - errorSimulator = ErrorSimulator(value: errorValue, description: description, statusCode: 401, shouldPerformRequest: false, stubData: nil) + errorSimulator = ErrorSimulator(value: errorValue, description: description, statusCode: statusCode, shouldPerformRequest: false, stubDataDict: data) } } @@ -1075,7 +1099,6 @@ class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor { errorSimulator = ErrorSimulator(value: 0, description: "", statusCode: 200, shouldPerformRequest: false, stubData: data) } } - } /// Records each message for test purpose. diff --git a/Test/AblyTests/Tests/UtilitiesTests.swift b/Test/AblyTests/Tests/UtilitiesTests.swift index b10c188b8..30837dc62 100644 --- a/Test/AblyTests/Tests/UtilitiesTests.swift +++ b/Test/AblyTests/Tests/UtilitiesTests.swift @@ -239,6 +239,73 @@ class UtilitiesTests: XCTestCase { } } + func test__Utilities__JSON_Encoder__should_decode_rest_error_response_with_only_error_field() throws { + let test = Test() + beforeEach__Utilities__JSON_Encoder() + + let options = try AblyTests.commonAppSetup(for: test) + let rest = ARTRest(options: options) + let testHTTPExecutor = TestProxyHTTPExecutor(logger: .init(clientOptions: options)) + rest.internal.httpExecutor = testHTTPExecutor + + testHTTPExecutor.simulateIncomingServerErrorOnNextRequest( + 40400, + statusCode: 404, + description: "Not found", + data: [ + "err": "Error that shouldn't be parsed" + ] + ) + + let request = URLRequest(url: URL(string: "https://www.example.com")!) + waitUntil(timeout: testTimeout) { done in + rest.internal.execute(request, wrapperSDKAgents:nil, completion: { response, _, error in + guard let error = error as? ARTErrorInfo else { + fail("Should be ARTErrorInfo"); done(); return + } + XCTAssertTrue(error.code == 40400) + XCTAssertTrue(error.statusCode == 404) + XCTAssertTrue(error.message == "HTTP request failed with status code 404") + done() + }) + } + } + + func test__Utilities__JSON_Encoder__should_decode_rest_error_response_with_complete_error_info() throws { + let test = Test() + beforeEach__Utilities__JSON_Encoder() + + let options = try AblyTests.commonAppSetup(for: test) + let rest = ARTRest(options: options) + let testHTTPExecutor = TestProxyHTTPExecutor(logger: .init(clientOptions: options)) + rest.internal.httpExecutor = testHTTPExecutor + + testHTTPExecutor.simulateIncomingServerErrorOnNextRequest( + 40400, + statusCode: 404, + description: "Not found", + data: [ + "error": [ + "code": 40400, + "message": "Object not found" + ] + ] + ) + + let request = URLRequest(url: URL(string: "https://www.example.com")!) + waitUntil(timeout: testTimeout) { done in + rest.internal.execute(request, wrapperSDKAgents:nil, completion: { response, _, error in + guard let error = error as? ARTErrorInfo else { + fail("Should be ARTErrorInfo"); done(); return + } + XCTAssertTrue(error.code == 40400) + XCTAssertTrue(error.statusCode == 404) + XCTAssertTrue(error.message == "Object not found") + done() + }) + } + } + func beforeEach__Utilities__EventEmitter() { eventEmitter = ARTInternalEventEmitter(queue: AblyTests.queue) receivedFoo1 = nil