Add multipart response download task (2nd edition)
Reviewed By: mmmulani Differential Revision: D3976605 fbshipit-source-id: c15cc859aa1288e831f70256566f743f4a8d9cd2
This commit is contained in:
parent
584bd0da24
commit
84eaeb0adf
|
@ -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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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];
|
||||
// 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 @@
|
|||
|
||||
- (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,7 +71,10 @@
|
|||
[_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;
|
||||
|
@ -77,6 +82,7 @@
|
|||
}
|
||||
|
||||
if (range.location == NSNotFound) {
|
||||
bytesSeen = content.length;
|
||||
NSInteger bytesRead = [_stream read:buffer maxLength:bufferLen];
|
||||
if (bytesRead <= 0 || _stream.streamError) {
|
||||
return NO;
|
||||
|
@ -85,12 +91,13 @@
|
|||
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];
|
||||
}
|
||||
|
||||
|
@ -98,7 +105,7 @@
|
|||
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 */,
|
||||
|
|
Loading…
Reference in New Issue