From 69ec19c61e77324cfd6e0fba27775e186f2c9161 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Tue, 4 Oct 2016 15:08:26 -0700 Subject: [PATCH] Add multipart response download task Reviewed By: mmmulani Differential Revision: D3940132 fbshipit-source-id: 7a6543223cea2523bedc585f890c9f64df0509ff --- React/Base/RCTJavaScriptLoader.m | 86 +++++++++++----------- React/Base/RCTMultipartDataTask.h | 20 +++++ React/Base/RCTMultipartDataTask.m | 102 ++++++++++++++++++++++++++ React/React.xcodeproj/project.pbxproj | 6 ++ 4 files changed, 172 insertions(+), 42 deletions(-) create mode 100644 React/Base/RCTMultipartDataTask.h create mode 100644 React/Base/RCTMultipartDataTask.m diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index fd40d5a89..d24b49ce7 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -14,6 +14,7 @@ #import "RCTSourceCode.h" #import "RCTUtils.h" #import "RCTPerformanceLogger.h" +#import "RCTMultipartDataTask.h" #include @@ -151,50 +152,51 @@ static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoad return; } - // Load remote script file - NSURLSessionDataTask *task = - [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler: - ^(NSData *data, NSURLResponse *response, NSError *error) { - // Handle general request errors - if (error) { - if ([error.domain isEqualToString:NSURLErrorDomain]) { - error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain - code:RCTJavaScriptLoaderErrorURLLoadFailed - userInfo: - @{ - NSLocalizedDescriptionKey: - [@"Could not connect to development server.\n\n" - "Ensure the following:\n" - "- Node server is running and available on the same network - run 'npm start' from react-native root\n" - "- Node server URL is correctly set in AppDelegate\n\n" - "URL: " stringByAppendingString:scriptURL.absoluteString], - NSLocalizedFailureReasonErrorKey: error.localizedDescription, - NSUnderlyingErrorKey: error, - }]; - } - onComplete(error, nil, 0); - return; - } + RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) { + if (!done) { + // TODO(frantic): Emit progress event + return; + } + + // Handle general request errors + if (error) { + if ([error.domain isEqualToString:NSURLErrorDomain]) { + error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain + code:RCTJavaScriptLoaderErrorURLLoadFailed + userInfo: + @{ + NSLocalizedDescriptionKey: + [@"Could not connect to development server.\n\n" + "Ensure the following:\n" + "- Node server is running and available on the same network - run 'npm start' from react-native root\n" + "- Node server URL is correctly set in AppDelegate\n\n" + "URL: " stringByAppendingString:scriptURL.absoluteString], + NSLocalizedFailureReasonErrorKey: error.localizedDescription, + NSUnderlyingErrorKey: error, + }]; + } + onComplete(error, nil, 0); + return; + } + + // For multipart responses packager sets X-Http-Status header in case HTTP status code + // is different from 200 OK + NSString *statusCodeHeader = [headers valueForKey:@"X-Http-Status"]; + if (statusCodeHeader) { + statusCode = [statusCodeHeader integerValue]; + } + + if (statusCode != 200) { + error = [NSError errorWithDomain:@"JSServer" + code:statusCode + userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding])]; + onComplete(error, nil, 0); + return; + } + onComplete(nil, data, data.length); + }]; - // Parse response as text - NSStringEncoding encoding = NSUTF8StringEncoding; - if (response.textEncodingName != nil) { - CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); - if (cfEncoding != kCFStringEncodingInvalidId) { - encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); - } - } - // Handle HTTP errors - if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) { - error = [NSError errorWithDomain:@"JSServer" - code:((NSHTTPURLResponse *)response).statusCode - userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data encoding:encoding])]; - onComplete(error, nil, 0); - return; - } - onComplete(nil, data, data.length); - }]; [task resume]; } diff --git a/React/Base/RCTMultipartDataTask.h b/React/Base/RCTMultipartDataTask.h new file mode 100644 index 000000000..246fba1b3 --- /dev/null +++ b/React/Base/RCTMultipartDataTask.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import "RCTMultipartStreamReader.h" + +typedef void (^RCTMultipartDataTaskCallback)(NSInteger statusCode, NSDictionary *headers, NSData *content, NSError *error, BOOL done); + +@interface RCTMultipartDataTask : NSObject + +- (instancetype)initWithURL:(NSURL *)url partHandler:(RCTMultipartDataTaskCallback)partHandler; +- (void)resume; + +@end diff --git a/React/Base/RCTMultipartDataTask.m b/React/Base/RCTMultipartDataTask.m new file mode 100644 index 000000000..98a341259 --- /dev/null +++ b/React/Base/RCTMultipartDataTask.m @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTMultipartDataTask.h" + +@interface RCTMultipartDataTask () + +@end + +@implementation RCTMultipartDataTask { + NSURLSessionDataTask *_dataTask; + RCTMultipartDataTaskCallback _partHandler; + NSInteger _statusCode; + NSDictionary *_headers; + NSString *_boundary; + NSMutableData *_data; +} + +- (instancetype)initWithURL:(NSURL *)url partHandler:(RCTMultipartDataTaskCallback)partHandler +{ + if (self = [super init]) { + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:self delegateQueue:nil]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request addValue:@"multipart/mixed" forHTTPHeaderField:@"Accept"]; + _dataTask = [session dataTaskWithRequest:request]; + _partHandler = [partHandler copy]; + } + return self; +} + +- (void)resume +{ + [_dataTask resume]; +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler +{ + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + _headers = [httpResponse allHeaderFields]; + _statusCode = [httpResponse statusCode]; + + NSString *contentType = @""; + for (NSString *key in [_headers keyEnumerator]) { + if ([[key lowercaseString] isEqualToString:@"content-type"]) { + contentType = [_headers valueForKey:key]; + break; + } + } + + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"multipart/mixed;.*boundary=\"([^\"]+)\"" options:0 error:nil]; + NSTextCheckingResult *match = [regex firstMatchInString:contentType options:0 range:NSMakeRange(0, contentType.length)]; + if (match) { + _boundary = [contentType substringWithRange:[match rangeAtIndex:1]]; + completionHandler(NSURLSessionResponseBecomeStream); + return; + } + } + + // In case the server doesn't support multipart/mixed responses, fallback to normal download + _data = [[NSMutableData alloc] initWithCapacity:1024 * 1024]; + completionHandler(NSURLSessionResponseAllow); +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error +{ + _partHandler(_statusCode, _headers, _data, error, YES); +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data +{ + [_data appendData:data]; +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeStreamTask:(NSURLSessionStreamTask *)streamTask +{ + [streamTask captureStreams]; +} + +- (void)URLSession:(NSURLSession *)session streamTask:(NSURLSessionStreamTask *)streamTask didBecomeInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream +{ + RCTMultipartStreamReader *reader = [[RCTMultipartStreamReader alloc] initWithInputStream:inputStream boundary:_boundary]; + RCTMultipartDataTaskCallback partHandler = _partHandler; + NSInteger statusCode = _statusCode; + + BOOL completed = [reader readAllParts:^(NSDictionary *headers, NSData *content, BOOL done) { + partHandler(statusCode, headers, content, nil, done); + }]; + if (!completed) { + partHandler(statusCode, nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil], YES); + } +} + + +@end diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index a2ca6a696..5adc58708 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; }; 001BFCD01D8381DE008E587E /* RCTMultipartStreamReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */; }; + 006FC4141D9B20820057AAAD /* RCTMultipartDataTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 006FC4131D9B20820057AAAD /* RCTMultipartDataTask.m */; }; 008341F61D1DB34400876D9A /* RCTJSStackFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 008341F41D1DB34400876D9A /* RCTJSStackFrame.m */; }; 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; }; 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */; }; @@ -125,6 +126,8 @@ 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = ""; }; 001BFCCE1D8381DE008E587E /* RCTMultipartStreamReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMultipartStreamReader.h; sourceTree = ""; }; 001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultipartStreamReader.m; sourceTree = ""; }; + 006FC4121D9B20820057AAAD /* RCTMultipartDataTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMultipartDataTask.h; sourceTree = ""; }; + 006FC4131D9B20820057AAAD /* RCTMultipartDataTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultipartDataTask.m; sourceTree = ""; }; 008341F41D1DB34400876D9A /* RCTJSStackFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSStackFrame.m; sourceTree = ""; }; 008341F51D1DB34400876D9A /* RCTJSStackFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSStackFrame.h; sourceTree = ""; }; 131541CF1D3E4893006A0E08 /* CSSLayout-internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CSSLayout-internal.h"; sourceTree = ""; }; @@ -617,6 +620,8 @@ 14C2CA731B3AC64300E6CBB2 /* RCTModuleData.mm */, 14C2CA6F1B3AC63800E6CBB2 /* RCTModuleMethod.h */, 14C2CA701B3AC63800E6CBB2 /* RCTModuleMethod.m */, + 006FC4121D9B20820057AAAD /* RCTMultipartDataTask.h */, + 006FC4131D9B20820057AAAD /* RCTMultipartDataTask.m */, 001BFCCE1D8381DE008E587E /* RCTMultipartStreamReader.h */, 001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */, 13A6E20F1C19ABC700845B82 /* RCTNullability.h */, @@ -749,6 +754,7 @@ 13A0C28A1B74F71200B29F6F /* RCTDevMenu.m in Sources */, 13BCE8091C99CB9D00DD7AAD /* RCTRootShadowView.m in Sources */, 14C2CA711B3AC63800E6CBB2 /* RCTModuleMethod.m in Sources */, + 006FC4141D9B20820057AAAD /* RCTMultipartDataTask.m in Sources */, 1321C8D01D3EB50800D58318 /* CSSNodeList.c in Sources */, 13CC8A821B17642100940AE7 /* RCTBorderDrawing.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */,