Permalink
Browse files

Add multipart response download task (2nd edition)

Reviewed By: mmmulani

Differential Revision: D3976605

fbshipit-source-id: c15cc859aa1288e831f70256566f743f4a8d9cd2
  • Loading branch information...
frantic authored and Facebook Github Bot committed Oct 11, 2016
1 parent 584bd0d commit 84eaeb0adff783e1d02090f7a1e92ddae56be26c
@@ -14,6 +14,7 @@
#import "RCTSourceCode.h"
#import "RCTUtils.h"
#import "RCTPerformanceLogger.h"
+#import "RCTMultipartDataTask.h"
#include <sys/stat.h>
@@ -151,51 +152,52 @@ 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;
- }
-
- // 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];
+
+ 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);
+ }];
+
+ [task startTask];
}
static NSURL *sanitizeURL(NSURL *url)
@@ -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 <Foundation/Foundation.h>
+#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)startTask;
+
+@end
@@ -0,0 +1,119 @@
+/**
+ * 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 () <NSURLSessionDataDelegate, NSURLSessionDataDelegate>
+
+@end
+
+// We need this ugly runtime check because [streamTask captureStreams] below fails on iOS version
+// earlier than 9.0. Unfortunately none of the proper ways of checking worked:
+//
+// - NSURLSessionStreamTask class is available and is not Null on iOS 8
+// - [[NSURLSessionStreamTask new] respondsToSelector:@selector(captureStreams)] is always NO
+// - The instance we get in URLSession:dataTask:didBecomeStreamTask: is of __NSCFURLLocalStreamTaskFromDataTask
+// and it responds to captureStreams on iOS 9+ but doesn't on iOS 8. Which means we can't get direct access
+// to the streams on iOS 8 and at that point it's too late to change the behavior back to dataTask
+// - The compile-time #ifdef's can't be used because an app compiled for iOS8 can still run on iOS9
+
+static BOOL isStreamTaskSupported() {
+ return [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}];
+}
+
+@implementation RCTMultipartDataTask {
+ NSURL *_url;
+ RCTMultipartDataTaskCallback _partHandler;
+ NSInteger _statusCode;
+ NSDictionary *_headers;
+ NSString *_boundary;
+ NSMutableData *_data;
+}
+
+- (instancetype)initWithURL:(NSURL *)url partHandler:(RCTMultipartDataTaskCallback)partHandler
+{
+ if (self = [super init]) {
+ _url = url;
+ _partHandler = [partHandler copy];
+ }
+ return self;
+}
+
+- (void)startTask
+{
+ NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
+ delegate:self delegateQueue:nil];
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url];
+ if (isStreamTaskSupported()) {
+ [request addValue:@"multipart/mixed" forHTTPHeaderField:@"Accept"];
+ }
+ NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
+ [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
@@ -58,7 +58,9 @@ - (void)emitChunk:(NSData *)data callback:(RCTMultipartCallback)callback done:(B
- (BOOL)readAllParts:(RCTMultipartCallback)callback
{
- NSInteger start = 0;
+ NSInteger chunkStart = 0;
+ NSInteger bytesSeen = 0;
+
NSData *delimiter = [[NSString stringWithFormat:@"%@--%@%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
NSData *closeDelimiter = [[NSString stringWithFormat:@"%@--%@--%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *content = [[NSMutableData alloc] initWithCapacity:1];
@@ -69,14 +71,18 @@ - (BOOL)readAllParts:(RCTMultipartCallback)callback
[_stream open];
while (true) {
BOOL isCloseDelimiter = NO;
- NSRange remainingBufferRange = NSMakeRange(start, content.length - start);
+ // Search only a subset of chunk that we haven't seen before + few bytes
+ // to allow for the edge case when the delimiter is cut by read call
+ NSInteger searchStart = MAX(bytesSeen - (NSInteger)closeDelimiter.length, chunkStart);
+ NSRange remainingBufferRange = NSMakeRange(searchStart, content.length - searchStart);
NSRange range = [content rangeOfData:delimiter options:0 range:remainingBufferRange];
if (range.location == NSNotFound) {
isCloseDelimiter = YES;
range = [content rangeOfData:closeDelimiter options:0 range:remainingBufferRange];
}
if (range.location == NSNotFound) {
+ bytesSeen = content.length;
NSInteger bytesRead = [_stream read:buffer maxLength:bufferLen];
if (bytesRead <= 0 || _stream.streamError) {
return NO;
@@ -85,20 +91,21 @@ - (BOOL)readAllParts:(RCTMultipartCallback)callback
continue;
}
- NSInteger end = range.location;
- NSInteger length = end - start;
+ NSInteger chunkEnd = range.location;
+ NSInteger length = chunkEnd - chunkStart;
+ bytesSeen = chunkEnd;
// Ignore preamble
- if (start > 0) {
- NSData *chunk = [content subdataWithRange:NSMakeRange(start, length)];
+ if (chunkStart > 0) {
+ NSData *chunk = [content subdataWithRange:NSMakeRange(chunkStart, length)];
[self emitChunk:chunk callback:callback done:isCloseDelimiter];
}
if (isCloseDelimiter) {
return YES;
}
- start = end + delimiter.length;
+ chunkStart = chunkEnd + delimiter.length;
}
}
@@ -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 */; };
@@ -219,6 +220,8 @@
000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = "<group>"; };
001BFCCE1D8381DE008E587E /* RCTMultipartStreamReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMultipartStreamReader.h; sourceTree = "<group>"; };
001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultipartStreamReader.m; sourceTree = "<group>"; };
+ 006FC4121D9B20820057AAAD /* RCTMultipartDataTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMultipartDataTask.h; sourceTree = "<group>"; };
+ 006FC4131D9B20820057AAAD /* RCTMultipartDataTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMultipartDataTask.m; sourceTree = "<group>"; };
008341F41D1DB34400876D9A /* RCTJSStackFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSStackFrame.m; sourceTree = "<group>"; };
008341F51D1DB34400876D9A /* RCTJSStackFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSStackFrame.h; sourceTree = "<group>"; };
131541CF1D3E4893006A0E08 /* CSSLayout-internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CSSLayout-internal.h"; sourceTree = "<group>"; };
@@ -720,6 +723,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 */,
@@ -994,6 +999,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 */,

0 comments on commit 84eaeb0

Please sign in to comment.