Skip to content
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
23 changes: 18 additions & 5 deletions Source/ARTJsonLikeEncoder.m
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 13 additions & 8 deletions Source/ARTRest.m
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,9 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary<NSStrin
if (!validContentType) {
NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// Construct artificial error
error = [ARTErrorInfo createWithCode:response.statusCode * 100 status:response.statusCode message:[plain art_shortString] requestId:requestId];
error = [ARTErrorInfo createWithCode:response.statusCode * 100
status:response.statusCode
message:[plain art_shortString] requestId:requestId];
data = nil; // Discard data; format is unreliable.
ARTLogError(self.logger, @"Request %@ failed with %@", request, error);
}
Expand All @@ -411,7 +413,9 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary<NSStrin
if (response.statusCode >= 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
Expand All @@ -429,11 +433,10 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary<NSStrin
}
if (!error) {
// Return error with HTTP StatusCode if ARTErrorStatusCode does not exist
error = [ARTErrorInfo
createWithCode:response.statusCode*100
status:response.statusCode
message:[[NSString alloc] initWithData:data ?: [NSData data] encoding:NSUTF8StringEncoding]
requestId:requestId];
error = [ARTErrorInfo createWithCode:response.statusCode * 100
status:response.statusCode
message:[[NSString alloc] initWithData:data ?: [NSData data] encoding:NSUTF8StringEncoding]
requestId:requestId];
}

} else {
Expand Down Expand Up @@ -561,7 +564,9 @@ - (void)timeWithWrapperSDKAgents:(nullable NSStringDictionary *)wrapperSDKAgents
}
NSError *decodeError = nil;
if (response.statusCode >= 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];
Expand Down
2 changes: 1 addition & 1 deletion Source/PrivateHeaders/Ably/ARTEncoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 39 additions & 16 deletions Test/AblyTests/Test Utilities/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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.
Expand Down
67 changes: 67 additions & 0 deletions Test/AblyTests/Tests/UtilitiesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading