diff --git a/packages/react-native/Libraries/WebSocket/RCTReconnectingWebSocket.m b/packages/react-native/Libraries/WebSocket/RCTReconnectingWebSocket.m index 1bd4de74a724..ce64e0af0aca 100644 --- a/packages/react-native/Libraries/WebSocket/RCTReconnectingWebSocket.m +++ b/packages/react-native/Libraries/WebSocket/RCTReconnectingWebSocket.m @@ -9,6 +9,7 @@ #import #import +#import #import @@ -46,7 +47,9 @@ - (void)start { [self stop]; _stopped = NO; - _socket = [[SRWebSocket alloc] initWithURL:_url]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url]; + [[RCTDevSupportHttpHeaders sharedInstance] applyHeadersToRequest:request]; + _socket = [[SRWebSocket alloc] initWithURLRequest:request]; _socket.delegate = self; [_socket setDelegateDispatchQueue:_delegateDispatchQueue]; [_socket open]; diff --git a/packages/react-native/React/Base/RCTBundleURLProvider.mm b/packages/react-native/React/Base/RCTBundleURLProvider.mm index 89ccd0c7ad7a..dd708d4a0d6b 100644 --- a/packages/react-native/React/Base/RCTBundleURLProvider.mm +++ b/packages/react-native/React/Base/RCTBundleURLProvider.mm @@ -10,6 +10,7 @@ #import "RCTConstants.h" #import "RCTConvert.h" #import "RCTDefines.h" +#import "RCTDevSupportHttpHeaders.h" #import "RCTLog.h" #import @@ -93,9 +94,10 @@ + (BOOL)isPackagerRunning:(NSString *)hostPort scheme:(NSString *)scheme NSURL *url = [serverRootWithHostPort(hostPort, scheme) URLByAppendingPathComponent:@"status"]; NSURLSession *session = [NSURLSession sharedSession]; - NSURLRequest *request = [NSURLRequest requestWithURL:url - cachePolicy:NSURLRequestUseProtocolCachePolicy - timeoutInterval:10]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:10]; + [[RCTDevSupportHttpHeaders sharedInstance] applyHeadersToRequest:request]; __block NSURLResponse *response; __block NSData *data; diff --git a/packages/react-native/React/Base/RCTDevSupportHttpHeaders.h b/packages/react-native/React/Base/RCTDevSupportHttpHeaders.h new file mode 100644 index 000000000000..e73cdd3c4b88 --- /dev/null +++ b/packages/react-native/React/Base/RCTDevSupportHttpHeaders.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + * Thread-safe singleton that holds custom HTTP headers to be applied + * to all devsupport network requests (bundle fetches, packager status + * checks, inspector and HMR WebSocket connections). + */ +@interface RCTDevSupportHttpHeaders : NSObject + ++ (instancetype)sharedInstance; + +- (void)addRequestHeader:(NSString *)name value:(NSString *)value; +- (void)removeRequestHeader:(NSString *)name; +- (NSDictionary *)allHeaders; +- (void)applyHeadersToRequest:(NSMutableURLRequest *)request; + +@end diff --git a/packages/react-native/React/Base/RCTDevSupportHttpHeaders.m b/packages/react-native/React/Base/RCTDevSupportHttpHeaders.m new file mode 100644 index 000000000000..0717537f8424 --- /dev/null +++ b/packages/react-native/React/Base/RCTDevSupportHttpHeaders.m @@ -0,0 +1,65 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTDevSupportHttpHeaders.h" + +@implementation RCTDevSupportHttpHeaders { + NSMutableDictionary *_headers; + dispatch_queue_t _queue; +} + ++ (instancetype)sharedInstance +{ + static RCTDevSupportHttpHeaders *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[RCTDevSupportHttpHeaders alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init +{ + if (self = [super init]) { + _headers = [NSMutableDictionary new]; + _queue = dispatch_queue_create("com.facebook.react.RCTDevSupportHttpHeaders", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +- (void)addRequestHeader:(NSString *)name value:(NSString *)value +{ + dispatch_sync(_queue, ^{ + self->_headers[name] = value; + }); +} + +- (void)removeRequestHeader:(NSString *)name +{ + dispatch_sync(_queue, ^{ + [self->_headers removeObjectForKey:name]; + }); +} + +- (NSDictionary *)allHeaders +{ + __block NSDictionary *snapshot; + dispatch_sync(_queue, ^{ + snapshot = [self->_headers copy]; + }); + return snapshot; +} + +- (void)applyHeadersToRequest:(NSMutableURLRequest *)request +{ + NSDictionary *headers = [self allHeaders]; + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { + [request setValue:headerValue forHTTPHeaderField:headerName]; + }]; +} + +@end diff --git a/packages/react-native/React/Base/RCTMultipartDataTask.m b/packages/react-native/React/Base/RCTMultipartDataTask.m index 3ac07683eb93..9f5ff5995535 100644 --- a/packages/react-native/React/Base/RCTMultipartDataTask.m +++ b/packages/react-native/React/Base/RCTMultipartDataTask.m @@ -7,6 +7,8 @@ #import "RCTMultipartDataTask.h" +#import "RCTDevSupportHttpHeaders.h" + @interface RCTMultipartDataTask () @end @@ -40,6 +42,7 @@ - (void)startTask delegateQueue:nil]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url]; [request addValue:@"multipart/mixed" forHTTPHeaderField:@"Accept"]; + [[RCTDevSupportHttpHeaders sharedInstance] applyHeadersToRequest:request]; NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request]; [dataTask resume]; [session finishTasksAndInvalidate]; diff --git a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm index 9230dd3b3322..36e18415eed3 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -14,6 +14,7 @@ #import #import +#import #import #import @@ -154,6 +155,7 @@ + (void)openDebugger:(NSURL *)bundleURL withErrorMessage:(NSString *)errorMessag escapedInspectorDeviceId]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; + [[RCTDevSupportHttpHeaders sharedInstance] applyHeadersToRequest:request]; [[[NSURLSession sharedSession] dataTaskWithRequest:request diff --git a/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm index 8e775d3185df..f0fa55333445 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm +++ b/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm @@ -6,6 +6,7 @@ */ #import "RCTInspectorNetworkHelper.h" +#import #import using ListenerBlock = void (^)(RCTInspectorNetworkListener *); @@ -47,6 +48,7 @@ - (void)loadNetworkResourceWithParams:(const RCTInspectorLoadNetworkResourceRequ NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url]; [urlRequest setHTTPMethod:@"GET"]; + [[RCTDevSupportHttpHeaders sharedInstance] applyHeadersToRequest:urlRequest]; NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:urlRequest]; __weak NSURLSessionDataTask *weakDataTask = dataTask; diff --git a/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm b/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm index 4b0754b26ae1..ad9db108e20e 100644 --- a/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm +++ b/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm @@ -9,6 +9,7 @@ #if RCT_DEV || RCT_REMOTE_PROFILE +#import #import #import #import @@ -36,7 +37,10 @@ - (instancetype)initWithURL:(const std::string &)url delegate:(std::weak_ptr() + + private val headerInterceptor = Interceptor { chain -> + val builder = chain.request().newBuilder() + for ((name, value) in customHeaders) { + builder.header(name, value) + } + chain.proceed(builder.build()) + } + + /** Client for HTTP requests: connect=5s, write=disabled, read=disabled. */ + public val httpClient: OkHttpClient = + OkHttpClient.Builder() + .addInterceptor(headerInterceptor) + .connectTimeout(5, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.MILLISECONDS) + .readTimeout(0, TimeUnit.MINUTES) + .build() + + /** Client for WebSocket connections: connect=10s, write=10s, read=disabled. */ + public val websocketClient: OkHttpClient = + httpClient + .newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build() + + /** Add a custom header to be included in all requests made through both clients. */ + @JvmStatic + public fun addRequestHeader(name: String, value: String) { + customHeaders[name] = value + } + + /** Remove a previously added custom header. */ + @JvmStatic + public fun removeRequestHeader(name: String) { + customHeaders.remove(name) + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/InspectorNetworkHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/InspectorNetworkHelper.kt index a7ba76b09e03..1dab02ea9dc4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/InspectorNetworkHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/InspectorNetworkHelper.kt @@ -10,26 +10,15 @@ package com.facebook.react.devsupport.inspector import java.io.IOException -import java.util.concurrent.TimeUnit import okhttp3.Call import okhttp3.Callback -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response internal object InspectorNetworkHelper { - private lateinit var client: OkHttpClient @JvmStatic fun loadNetworkResource(url: String, listener: InspectorNetworkRequestListener) { - if (!::client.isInitialized) { - client = - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read - .build() - } val request = try { @@ -40,7 +29,7 @@ internal object InspectorNetworkHelper { } // TODO(T196951523): Assign cancel function to listener - val call = client.newCall(request) + val call = DevSupportHttpClient.httpClient.newCall(request) call.enqueue( object : Callback { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt index d122467eca71..87d80827a336 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/packagerconnection/ReconnectingWebSocket.kt @@ -10,10 +10,9 @@ package com.facebook.react.packagerconnection import android.os.Handler import android.os.Looper import com.facebook.common.logging.FLog +import com.facebook.react.devsupport.inspector.DevSupportHttpClient import java.io.IOException import java.nio.channels.ClosedChannelException -import java.util.concurrent.TimeUnit -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket @@ -40,12 +39,7 @@ public class ReconnectingWebSocket( } private val handler = Handler(Looper.getMainLooper()) - private val okHttpClient: OkHttpClient = - OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read - .build() + private val okHttpClient = DevSupportHttpClient.websocketClient private var closed = false private var suppressConnectionErrors = false private var webSocket: WebSocket? = null