Add multipart response download task

Reviewed By: mmmulani

Differential Revision: D3940132

fbshipit-source-id: 7a6543223cea2523bedc585f890c9f64df0509ff
This commit is contained in:
Alex Kotliarskyi 2016-10-04 15:08:26 -07:00 committed by Facebook Github Bot
parent f7cbd56d8e
commit 69ec19c61e
4 changed files with 172 additions and 42 deletions

View File

@ -14,6 +14,7 @@
#import "RCTSourceCode.h"
#import "RCTUtils.h"
#import "RCTPerformanceLogger.h"
#import "RCTMultipartDataTask.h"
#include <sys/stat.h>
@ -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];
}

View File

@ -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)resume;
@end

View File

@ -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 () <NSURLSessionDataDelegate, NSURLSessionDataDelegate>
@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

View File

@ -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 = "<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>"; };
@ -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 */,