From 8cf0d21a7a6ca3e09db5bcb1b6ff29c8d095114e Mon Sep 17 00:00:00 2001 From: Sefa Ilkimen Date: Wed, 12 Jun 2019 23:43:40 +0200 Subject: [PATCH] add response type "arraybuffer" support for iOS --- plugin.xml | 4 +- .../AFNetworking/AFURLResponseSerialization.h | 7 + .../AFNetworking/AFURLResponseSerialization.m | 5 +- src/ios/BinaryResponseSerializer.h | 8 ++ src/ios/BinaryResponseSerializer.m | 126 ++++++++++++++++++ src/ios/CordovaHttpPlugin.m | 36 +++-- src/ios/TextResponseSerializer.h | 2 - src/ios/TextResponseSerializer.m | 18 +-- test/e2e-specs.js | 16 ++- 9 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 src/ios/BinaryResponseSerializer.h create mode 100644 src/ios/BinaryResponseSerializer.m diff --git a/plugin.xml b/plugin.xml index 2b8189e..338ce56 100644 --- a/plugin.xml +++ b/plugin.xml @@ -28,6 +28,7 @@ + @@ -39,6 +40,7 @@ + @@ -87,4 +89,4 @@ - \ No newline at end of file + diff --git a/src/ios/AFNetworking/AFURLResponseSerialization.h b/src/ios/AFNetworking/AFURLResponseSerialization.h index a9430ad..10e0fef 100644 --- a/src/ios/AFNetworking/AFURLResponseSerialization.h +++ b/src/ios/AFNetworking/AFURLResponseSerialization.h @@ -308,4 +308,11 @@ FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseErrorK FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey; +/** +`AFNetworkingOperationFailingURLResponseBodyErrorKey` +The corresponding value is an `NSString` containing the decoded error message. + */ + +FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseBodyErrorKey; + NS_ASSUME_NONNULL_END diff --git a/src/ios/AFNetworking/AFURLResponseSerialization.m b/src/ios/AFNetworking/AFURLResponseSerialization.m index 5e46799..f88d938 100755 --- a/src/ios/AFNetworking/AFURLResponseSerialization.m +++ b/src/ios/AFNetworking/AFURLResponseSerialization.m @@ -34,6 +34,7 @@ NSString * const AFURLResponseSerializationErrorDomain = @"com.alamofire.error.serialization.response"; NSString * const AFNetworkingOperationFailingURLResponseErrorKey = @"com.alamofire.serialization.response.error.response"; NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey = @"com.alamofire.serialization.response.error.data"; +NSString * const AFNetworkingOperationFailingURLResponseBodyErrorKey = @"com.alamofire.serialization.response.error.body"; static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) { if (!error) { @@ -525,7 +526,7 @@ static NSLock* imageLock = nil; dispatch_once(&onceToken, ^{ imageLock = [[NSLock alloc] init]; }); - + [imageLock lock]; image = [UIImage imageWithData:data]; [imageLock unlock]; @@ -539,7 +540,7 @@ static UIImage * AFImageWithDataAtScale(NSData *data, CGFloat scale) { if (image.images) { return image; } - + return [[UIImage alloc] initWithCGImage:[image CGImage] scale:scale orientation:image.imageOrientation]; } diff --git a/src/ios/BinaryResponseSerializer.h b/src/ios/BinaryResponseSerializer.h new file mode 100644 index 0000000..92af266 --- /dev/null +++ b/src/ios/BinaryResponseSerializer.h @@ -0,0 +1,8 @@ +#import +#import "AFURLResponseSerialization.h" + +@interface BinaryResponseSerializer : AFHTTPResponseSerializer + ++ (instancetype)serializer; + +@end diff --git a/src/ios/BinaryResponseSerializer.m b/src/ios/BinaryResponseSerializer.m new file mode 100644 index 0000000..a891f8f --- /dev/null +++ b/src/ios/BinaryResponseSerializer.m @@ -0,0 +1,126 @@ +#import "BinaryResponseSerializer.h" + +static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) { + if (!error) { + return underlyingError; + } + + if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) { + return error; + } + + NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy]; + mutableUserInfo[NSUnderlyingErrorKey] = underlyingError; + + return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo]; +} + +static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) { + if ([error.domain isEqualToString:domain] && error.code == code) { + return YES; + } else if (error.userInfo[NSUnderlyingErrorKey]) { + return AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain); + } + + return NO; +} + +@implementation BinaryResponseSerializer + ++ (instancetype)serializer { + BinaryResponseSerializer *serializer = [[self alloc] init]; + return serializer; +} + +- (instancetype)init { + self = [super init]; + + if (!self) { + return nil; + } + + self.acceptableContentTypes = nil; + + return self; +} + +- (NSString*)decodeResponseData:(NSData*)rawResponseData withEncoding:(CFStringEncoding)cfEncoding { + NSStringEncoding nsEncoding; + NSString* decoded = nil; + + if (cfEncoding != kCFStringEncodingInvalidId) { + nsEncoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); + } + + NSStringEncoding supportedEncodings[6] = { + NSUTF8StringEncoding, NSWindowsCP1252StringEncoding, NSISOLatin1StringEncoding, + NSISOLatin2StringEncoding, NSASCIIStringEncoding, NSUnicodeStringEncoding + }; + + for (int i = 0; i < sizeof(supportedEncodings) / sizeof(NSStringEncoding) && !decoded; ++i) { + if (cfEncoding == kCFStringEncodingInvalidId || nsEncoding == supportedEncodings[i]) { + decoded = [[NSString alloc] initWithData:rawResponseData encoding:supportedEncodings[i]]; + } + } + + return decoded; +} + +- (CFStringEncoding) getEncoding:(NSURLResponse *)response { + CFStringEncoding encoding = kCFStringEncodingInvalidId; + + if (response.textEncodingName) { + encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + } + + return encoding; +} + +#pragma mark - + +- (BOOL)validateResponse:(NSHTTPURLResponse *)response + data:(NSData *)data + error:(NSError * __autoreleasing *)error +{ + if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) { + if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) { + NSMutableDictionary *mutableUserInfo = [@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode], + NSURLErrorFailingURLErrorKey: [response URL], + AFNetworkingOperationFailingURLResponseErrorKey: response, + } mutableCopy]; + + if (data) { + mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data; + + // trying to decode error message in body + mutableUserInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] = [self decodeResponseData:data withEncoding:[self getEncoding:response]]; + } + + if (error) { + *error = [NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo]; + } + + return NO; + } + } + + return YES; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) { + if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) { + return nil; + } + } + + return [data base64EncodedStringWithOptions:0]; +} + +@end diff --git a/src/ios/CordovaHttpPlugin.m b/src/ios/CordovaHttpPlugin.m index ac1c99b..13d4369 100644 --- a/src/ios/CordovaHttpPlugin.m +++ b/src/ios/CordovaHttpPlugin.m @@ -1,5 +1,6 @@ #import "CordovaHttpPlugin.h" #import "CDVFile.h" +#import "BinaryResponseSerializer.h" #import "TextResponseSerializer.h" #import "TextRequestSerializer.h" #import "AFHTTPSessionManager.h" @@ -57,6 +58,15 @@ [manager.requestSerializer setTimeoutInterval:timeout]; } +- (void)setResponseSerializer:(NSString*)responseType forManager:(AFHTTPSessionManager*)manager { + if ([responseType isEqualToString: @"text"]) { + manager.responseSerializer = [TextResponseSerializer serializer]; + } else { + manager.responseSerializer = [BinaryResponseSerializer serializer]; + } +} + + - (void)handleSuccess:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response andData:(id)data { if (response != nil) { [dictionary setValue:response.URL.absoluteString forKey:@"url"]; @@ -74,8 +84,8 @@ [dictionary setValue:response.URL.absoluteString forKey:@"url"]; [dictionary setObject:[NSNumber numberWithInt:(int)response.statusCode] forKey:@"status"]; [dictionary setObject:[self copyHeaderFields:response.allHeaderFields] forKey:@"headers"]; - if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyKey]) { - [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyKey] forKey:@"error"]; + if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) { + [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"]; } } else { [dictionary setObject:[self getStatusCode:error] forKey:@"status"]; @@ -161,14 +171,15 @@ NSDictionary *headers = [command.arguments objectAtIndex:1]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:3] boolValue]; + NSString *responseType = [command.arguments objectAtIndex:4]; [self setRequestSerializer: @"default" forManager: manager]; [self setRequestHeaders: headers forManager: manager]; [self setTimeout:timeoutInSeconds forManager:manager]; [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -197,6 +208,7 @@ - (void)head:(CDVInvokedUrlCommand*)command { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.securityPolicy = securityPolicy; + manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSString *url = [command.arguments objectAtIndex:0]; NSDictionary *headers = [command.arguments objectAtIndex:1]; @@ -208,7 +220,6 @@ [self setRedirect:followRedirect forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -243,14 +254,15 @@ NSDictionary *headers = [command.arguments objectAtIndex:1]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:3] boolValue]; + NSString *responseType = [command.arguments objectAtIndex:4]; [self setRequestSerializer: @"default" forManager: manager]; [self setRequestHeaders: headers forManager: manager]; [self setTimeout:timeoutInSeconds forManager:manager]; [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -286,14 +298,15 @@ NSDictionary *headers = [command.arguments objectAtIndex:3]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:5] boolValue]; + NSString *responseType = [command.arguments objectAtIndex:6]; [self setRequestSerializer: serializerName forManager: manager]; [self setRequestHeaders: headers forManager: manager]; [self setTimeout:timeoutInSeconds forManager:manager]; [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -329,14 +342,15 @@ NSDictionary *headers = [command.arguments objectAtIndex:3]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:5] boolValue]; + NSString *responseType = [command.arguments objectAtIndex:6]; [self setRequestSerializer: serializerName forManager: manager]; [self setRequestHeaders: headers forManager: manager]; [self setTimeout:timeoutInSeconds forManager:manager]; [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -372,14 +386,15 @@ NSDictionary *headers = [command.arguments objectAtIndex:3]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:5] boolValue]; + NSString *responseType = [command.arguments objectAtIndex:6]; [self setRequestSerializer: serializerName forManager: manager]; [self setRequestHeaders: headers forManager: manager]; [self setTimeout:timeoutInSeconds forManager:manager]; [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -415,15 +430,16 @@ NSString *name = [command.arguments objectAtIndex: 3]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:5] boolValue]; + NSString *responseType = [command.arguments objectAtIndex:6]; NSURL *fileURL = [NSURL URLWithString: filePath]; [self setRequestHeaders: headers forManager: manager]; [self setTimeout:timeoutInSeconds forManager:manager]; [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -464,6 +480,7 @@ - (void)downloadFile:(CDVInvokedUrlCommand*)command { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.securityPolicy = securityPolicy; + manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSString *url = [command.arguments objectAtIndex:0]; NSDictionary *headers = [command.arguments objectAtIndex:1]; @@ -480,7 +497,6 @@ } CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { diff --git a/src/ios/TextResponseSerializer.h b/src/ios/TextResponseSerializer.h index 7bf87db..d086a8c 100644 --- a/src/ios/TextResponseSerializer.h +++ b/src/ios/TextResponseSerializer.h @@ -5,6 +5,4 @@ + (instancetype)serializer; -FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseBodyKey; - @end diff --git a/src/ios/TextResponseSerializer.m b/src/ios/TextResponseSerializer.m index 762f7c3..76b6530 100644 --- a/src/ios/TextResponseSerializer.m +++ b/src/ios/TextResponseSerializer.m @@ -1,8 +1,5 @@ #import "TextResponseSerializer.h" -NSString * const AFNetworkingOperationFailingURLResponseBodyKey = @"com.alamofire.serialization.response.error.body"; -NSStringEncoding const SupportedEncodings[6] = { NSUTF8StringEncoding, NSWindowsCP1252StringEncoding, NSISOLatin1StringEncoding, NSISOLatin2StringEncoding, NSASCIIStringEncoding, NSUnicodeStringEncoding }; - static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) { if (!error) { return underlyingError; @@ -55,9 +52,14 @@ static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger co nsEncoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } - for (int i = 0; i < sizeof(SupportedEncodings) / sizeof(NSStringEncoding) && !decoded; ++i) { - if (cfEncoding == kCFStringEncodingInvalidId || nsEncoding == SupportedEncodings[i]) { - decoded = [[NSString alloc] initWithData:rawResponseData encoding:SupportedEncodings[i]]; + NSStringEncoding supportedEncodings[6] = { + NSUTF8StringEncoding, NSWindowsCP1252StringEncoding, NSISOLatin1StringEncoding, + NSISOLatin2StringEncoding, NSASCIIStringEncoding, NSUnicodeStringEncoding + }; + + for (int i = 0; i < sizeof(supportedEncodings) / sizeof(NSStringEncoding) && !decoded; ++i) { + if (cfEncoding == kCFStringEncodingInvalidId || nsEncoding == supportedEncodings[i]) { + decoded = [[NSString alloc] initWithData:rawResponseData encoding:supportedEncodings[i]]; } } @@ -94,7 +96,7 @@ static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger co NSURLErrorFailingURLErrorKey:[response URL], AFNetworkingOperationFailingURLResponseErrorKey: response, AFNetworkingOperationFailingURLResponseDataErrorKey: data, - AFNetworkingOperationFailingURLResponseBodyKey: @"Could not decode response data due to invalid or unknown charset encoding", + AFNetworkingOperationFailingURLResponseBodyErrorKey: @"Could not decode response data due to invalid or unknown charset encoding", } mutableCopy]; validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError); @@ -108,7 +110,7 @@ static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger co if (data) { mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data; - mutableUserInfo[AFNetworkingOperationFailingURLResponseBodyKey] = *decoded; + mutableUserInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] = *decoded; } validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError); diff --git a/test/e2e-specs.js b/test/e2e-specs.js index 3fa85f9..34168b7 100644 --- a/test/e2e-specs.js +++ b/test/e2e-specs.js @@ -642,7 +642,7 @@ const tests = [ } }, { - description: 'should fetch binary correctly when response type "arraybuffer" is given', + description: 'should fetch binary correctly when response type is "arraybuffer"', expected: 'resolved: {"hash":-1032603775,"byteLength":35588}', func: function (resolve, reject) { var url = 'https://httpbin.org/image/jpeg'; @@ -660,6 +660,20 @@ const tests = [ result.data.hash.should.be.equal(-1032603775); result.data.byteLength.should.be.equal(35588); } + }, + { + description: 'should decode error body even if response type is "arraybuffer"', + expected: 'rejected: {"status": 418, ...', + func: function (resolve, reject) { + var url = 'https://httpbin.org/status/418'; + var options = { method: 'get', responseType: 'arraybuffer' }; + cordova.plugin.http.sendRequest(url, options, resolve, reject); + }, + validationFunc: function (driver, result) { + result.type.should.be.equal('rejected'); + result.data.status.should.be.equal(418); + result.data.error.should.be.equal("\n -=[ teapot ]=-\n\n _...._\n .' _ _ `.\n | .\"` ^ `\". _,\n \\_;`\"---\"`|//\n | ;/\n \\_ _/\n `\"\"\"`\n"); + } } // @TODO: not ready yet // {