Files
react-native/Libraries/Network/RCTHTTPRequestHandler.mm
T
Parker Bond 01c70f2fb9 Add allowsCellularAccess flag for iOS fetch calls (#24242)
Summary:
On iOS in React Native, immediately after successfully connecting to a WiFi network, if 4G is also on, it is possible that network requests sent via fetch get sent over 4G instead of WiFi for several seconds (between 1 and 40+ seconds depending on the device and network in my experience). If the calls are meant to be communicating with a local Wireless Device (such as a wireless router, etc.), then API calls that get sent over 4G to the internet are lost. Note that Reachability/NetInfo are saying that we are connected to a WiFi network and are not sufficient to prevent this from happening. To prevent this, I would like to be able to set an instance property that iOS exposes for network requests allowsCellularAccess to NO to ensure a request is never sent over 4G.

This code needs to be able to accept a flag to allow the user to override the default value of YES, but only when they explicitly opt in to this decision. I am not sure the best place to set and pass in this value, so I created [this issue](https://github.com/react-native-community/discussions-and-proposals/issues/113), where it was recommended that I make a PR and discuss how best to do this here. Before this PR could be merged we would need to allow the NO to be configurable in some way.

Additional information about the [allowsCellularAccess](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1409406-allowscellularaccess?language=objc) property can be found in the [Apple documentation.](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/Platform-SpecificNetworkingTechnologies/Platform-SpecificNetworkingTechnologies.html#//apple_ref/doc/uid/TP40010220-CH212-SW9)

[iOS] [Added] - Ability to force network requests to use WiFi using the allowsCellularAccess property. This can ensure that network requests are sent over WiFi if communicating with a local hardware device and is accomplished by setting a flag. Default behavior of allowing network connections over cellular networks when available is unchanged.
Pull Request resolved: https://github.com/facebook/react-native/pull/24242

Differential Revision: D15316760

Pulled By: cpojer

fbshipit-source-id: 96df43b4eff6b4301c853df6b2e5c6e1bb1abda0
2019-05-13 07:29:50 -07:00

182 lines
6.0 KiB
Plaintext

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTHTTPRequestHandler.h"
#import <mutex>
#import "RCTNetworking.h"
@interface RCTHTTPRequestHandler () <NSURLSessionDataDelegate>
@end
@implementation RCTHTTPRequestHandler
{
NSMapTable *_delegates;
NSURLSession *_session;
std::mutex _mutex;
}
@synthesize bridge = _bridge;
@synthesize methodQueue = _methodQueue;
RCT_EXPORT_MODULE()
- (void)invalidate
{
dispatch_async(self->_methodQueue, ^{
[self->_session invalidateAndCancel];
self->_session = nil;
});
}
- (BOOL)isValid
{
// if session == nil and delegates != nil, we've been invalidated
return _session || !_delegates;
}
#pragma mark - NSURLRequestHandler
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
static NSSet<NSString *> *schemes = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// technically, RCTHTTPRequestHandler can handle file:// as well,
// but it's less efficient than using RCTFileRequestHandler
schemes = [[NSSet alloc] initWithObjects:@"http", @"https", nil];
});
return [schemes containsObject:request.URL.scheme.lowercaseString];
}
- (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request
withDelegate:(id<RCTURLRequestDelegate>)delegate
{
// Lazy setup
if (!_session && [self isValid]) {
// You can override default NSURLSession instance property allowsCellularAccess (default value YES)
// by providing the following key to your RN project (edit ios/project/Info.plist file in Xcode):
// <key>ReactNetworkForceWifiOnly</key> <string>YES</string>
// This will set allowsCellularAccess to NO and force Wifi only for all network calls on iOS
// If you do not want to override default behavior, do nothing or set key with value "NO"
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *useWifiOnly = [infoDictionary objectForKey:@"ReactNetworkForceWifiOnly"];
NSOperationQueue *callbackQueue = [NSOperationQueue new];
callbackQueue.maxConcurrentOperationCount = 1;
callbackQueue.underlyingQueue = [[_bridge networking] methodQueue];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// set allowsCellularAccess to NO ONLY if key ReactNetworkForceWifiOnly exists AND string value is "YES"
NSString *compareKeyToForceWifiOnly = @"YES";
if (useWifiOnly) {
configuration.allowsCellularAccess = ![compareKeyToForceWifiOnly isEqualToString:useWifiOnly];
}
[configuration setHTTPShouldSetCookies:YES];
[configuration setHTTPCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];
[configuration setHTTPCookieStorage:[NSHTTPCookieStorage sharedHTTPCookieStorage]];
_session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:callbackQueue];
std::lock_guard<std::mutex> lock(_mutex);
_delegates = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory
valueOptions:NSPointerFunctionsStrongMemory
capacity:0];
}
__block NSURLSessionDataTask *task = nil;
dispatch_sync(self->_methodQueue, ^{
task = [self->_session dataTaskWithRequest:request];
});
{
std::lock_guard<std::mutex> lock(_mutex);
[_delegates setObject:delegate forKey:task];
}
[task resume];
return task;
}
- (void)cancelRequest:(NSURLSessionDataTask *)task
{
{
std::lock_guard<std::mutex> lock(_mutex);
[_delegates removeObjectForKey:task];
}
[task cancel];
}
#pragma mark - NSURLSession delegate
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
}
[delegate URLRequest:task didSendDataWithProgress:totalBytesSent];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler
{
// Reset the cookies on redirect.
// This is necessary because we're not letting iOS handle cookies by itself
NSMutableURLRequest *nextRequest = [request mutableCopy];
NSArray<NSHTTPCookie *> *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL];
nextRequest.allHTTPHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
completionHandler(nextRequest);
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)task
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
}
[delegate URLRequest:task didReceiveResponse:response];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)task
didReceiveData:(NSData *)data
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
}
[delegate URLRequest:task didReceiveData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
id<RCTURLRequestDelegate> delegate;
{
std::lock_guard<std::mutex> lock(_mutex);
delegate = [_delegates objectForKey:task];
[_delegates removeObjectForKey:task];
}
[delegate URLRequest:task didCompleteWithError:error];
}
@end