diff --git a/packages/react-native/Libraries/Core/setUpReactDevTools.js b/packages/react-native/Libraries/Core/setUpReactDevTools.js index 9bdf66b30184..92efa8df39fe 100644 --- a/packages/react-native/Libraries/Core/setUpReactDevTools.js +++ b/packages/react-native/Libraries/Core/setUpReactDevTools.js @@ -146,17 +146,36 @@ if (__DEV__) { ? guessHostFromDevServerUrl(devServer.url) : 'localhost'; - // Read the optional global variable for backward compatibility. - // It was added in https://github.com/facebook/react-native/commit/bf2b435322e89d0aeee8792b1c6e04656c2719a0. - const port = + // Derive scheme and port from the dev server URL when possible, + // falling back to ws://host:8097 for local development. + let wsScheme = 'ws'; + let port = 8097; + + if ( // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-use] window.__REACT_DEVTOOLS_PORT__ != null - ? window.__REACT_DEVTOOLS_PORT__ - : 8097; + ) { + // $FlowFixMe[prop-missing] + port = window.__REACT_DEVTOOLS_PORT__; + } else if (devServer.bundleLoadedFromServer) { + try { + const devUrl = new URL(devServer.url); + if (devUrl.protocol === 'https:') { + wsScheme = 'wss'; + } + if (devUrl.port) { + port = parseInt(devUrl.port, 10); + } else if (devUrl.protocol === 'https:') { + port = 443; + } else { + port = 80; + } + } catch (e) {} + } const WebSocket = require('../WebSocket/WebSocket').default; - ws = new WebSocket('ws://' + host + ':' + port); + ws = new WebSocket(wsScheme + '://' + host + ':' + port); ws.addEventListener('close', event => { isWebSocketOpen = false; }); diff --git a/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm b/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm index 0303970a2e47..e7017453b147 100644 --- a/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm +++ b/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm @@ -9,6 +9,7 @@ #import +#import #import #import @@ -99,7 +100,9 @@ - (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request withDelegate:(id *)allHeaders; - (void)applyHeadersToRequest:(NSMutableURLRequest *)request; diff --git a/packages/react-native/React/Base/RCTDevSupportHttpHeaders.m b/packages/react-native/React/Base/RCTDevSupportHttpHeaders.m index 0717537f8424..0b6d5c8275a7 100644 --- a/packages/react-native/React/Base/RCTDevSupportHttpHeaders.m +++ b/packages/react-native/React/Base/RCTDevSupportHttpHeaders.m @@ -9,6 +9,7 @@ @implementation RCTDevSupportHttpHeaders { NSMutableDictionary *_headers; + NSMutableDictionary *> *_hostHeaders; dispatch_queue_t _queue; } @@ -26,6 +27,7 @@ - (instancetype)init { if (self = [super init]) { _headers = [NSMutableDictionary new]; + _hostHeaders = [NSMutableDictionary new]; _queue = dispatch_queue_create("com.facebook.react.RCTDevSupportHttpHeaders", DISPATCH_QUEUE_SERIAL); } return self; @@ -38,6 +40,18 @@ - (void)addRequestHeader:(NSString *)name value:(NSString *)value }); } +- (void)addRequestHeader:(NSString *)name value:(NSString *)value forHost:(NSString *)host +{ + dispatch_sync(_queue, ^{ + NSMutableDictionary *headersForHost = self->_hostHeaders[host]; + if (headersForHost == nil) { + headersForHost = [NSMutableDictionary new]; + self->_hostHeaders[host] = headersForHost; + } + headersForHost[name] = value; + }); +} + - (void)removeRequestHeader:(NSString *)name { dispatch_sync(_queue, ^{ @@ -45,6 +59,19 @@ - (void)removeRequestHeader:(NSString *)name }); } +- (void)removeRequestHeader:(NSString *)name forHost:(NSString *)host +{ + dispatch_sync(_queue, ^{ + NSMutableDictionary *headersForHost = self->_hostHeaders[host]; + if (headersForHost != nil) { + [headersForHost removeObjectForKey:name]; + if (headersForHost.count == 0) { + [self->_hostHeaders removeObjectForKey:host]; + } + } + }); +} + - (NSDictionary *)allHeaders { __block NSDictionary *snapshot; @@ -56,8 +83,23 @@ - (void)removeRequestHeader:(NSString *)name - (void)applyHeadersToRequest:(NSMutableURLRequest *)request { - NSDictionary *headers = [self allHeaders]; - [headers enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { + __block NSDictionary *globalHeaders; + __block NSDictionary *hostSpecificHeaders; + + NSString *requestHost = request.URL.host; + + dispatch_sync(_queue, ^{ + globalHeaders = [self->_headers copy]; + if (requestHost != nil && self->_hostHeaders[requestHost] != nil) { + hostSpecificHeaders = [self->_hostHeaders[requestHost] copy]; + } + }); + + [globalHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { + [request setValue:headerValue forHTTPHeaderField:headerName]; + }]; + + [hostSpecificHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { [request setValue:headerValue forHTTPHeaderField:headerName]; }]; } diff --git a/packages/react-native/React/CoreModules/RCTWebSocketModule.mm b/packages/react-native/React/CoreModules/RCTWebSocketModule.mm index 27f53f88dd85..a3ca77ede88d 100644 --- a/packages/react-native/React/CoreModules/RCTWebSocketModule.mm +++ b/packages/react-native/React/CoreModules/RCTWebSocketModule.mm @@ -12,6 +12,7 @@ #import #import #import +#import #import #import @@ -114,6 +115,8 @@ - (void)invalidate }]; } + [[RCTDevSupportHttpHeaders sharedInstance] applyHeadersToRequest:request]; + SRWebSocket *webSocket = [[SRWebSocket alloc] initWithURLRequest:request protocols:protocols]; [webSocket setDelegateDispatchQueue:[self methodQueue]]; webSocket.delegate = self; diff --git a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm index 36e18415eed3..fdc5628137a0 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -23,25 +23,29 @@ static NSString *getServerHost(NSURL *bundleURL) { - NSNumber *port = @8081; - NSString *portStr = [[[NSProcessInfo processInfo] environment] objectForKey:@"RCT_METRO_PORT"]; - if ((portStr != nullptr) && [portStr length] > 0) { - port = [NSNumber numberWithInt:[portStr intValue]]; - } - if ([bundleURL port] != nullptr) { - port = [bundleURL port]; - } NSString *host = [bundleURL host]; if (host == nullptr) { host = @"localhost"; } - // this is consistent with the Android implementation, where http:// is the - // hardcoded implicit scheme for the debug server. Note, packagerURL - // technically looks like it could handle schemes/protocols other than HTTP, - // so rather than force HTTP, leave it be for now, in case someone is relying - // on that ability when developing against iOS. - return [NSString stringWithFormat:@"%@:%@", host, port]; + // Use explicit port from URL if available + if ([bundleURL port] != nullptr) { + return [NSString stringWithFormat:@"%@:%@", host, [bundleURL port]]; + } + + // Check environment variable + NSString *portStr = [[[NSProcessInfo processInfo] environment] objectForKey:@"RCT_METRO_PORT"]; + if ((portStr != nullptr) && [portStr length] > 0) { + return [NSString stringWithFormat:@"%@:%@", host, portStr]; + } + + // For https, omit port — the scheme implies 443 + if ([[bundleURL scheme] isEqualToString:@"https"]) { + return host; + } + + // Default to 8081 for local development (Metro's default port) + return [NSString stringWithFormat:@"%@:%@", host, @8081]; } static NSString *getSHA256(NSString *string) @@ -112,13 +116,15 @@ NSString *escapedInspectorDeviceId = [getInspectorDeviceId() stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; - return [NSURL - URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@&device=%@&profiling=%@", - getServerHost(bundleURL), - escapedDeviceName, - escapedAppName, - escapedInspectorDeviceId, - isProfilingBuild ? @"true" : @"false"]]; + NSString *scheme = [bundleURL scheme] ?: @"http"; + return + [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@/inspector/device?name=%@&app=%@&device=%@&profiling=%@", + scheme, + getServerHost(bundleURL), + escapedDeviceName, + escapedAppName, + escapedInspectorDeviceId, + isProfilingBuild ? @"true" : @"false"]]; } @implementation RCTInspectorDevServerHelper @@ -150,7 +156,9 @@ + (void)openDebugger:(NSURL *)bundleURL withErrorMessage:(NSString *)errorMessag NSString *escapedInspectorDeviceId = [getInspectorDeviceId() stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; - NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/open-debugger?device=%@", + NSString *scheme = [bundleURL scheme] ?: @"http"; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@/open-debugger?device=%@", + scheme, getServerHost(bundleURL), escapedInspectorDeviceId]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/DevSupportHttpClient.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/DevSupportHttpClient.kt index dae61b4309bd..01cb7eece216 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/DevSupportHttpClient.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/DevSupportHttpClient.kt @@ -21,12 +21,20 @@ import okhttp3.OkHttpClient */ public object DevSupportHttpClient { private val customHeaders = ConcurrentHashMap() + private val hostHeaders = ConcurrentHashMap>() private val headerInterceptor = Interceptor { chain -> - val builder = chain.request().newBuilder() + val request = chain.request() + val builder = request.newBuilder() for ((name, value) in customHeaders) { builder.header(name, value) } + val host = request.url().host() + hostHeaders[host]?.let { headersForHost -> + for ((name, value) in headersForHost) { + builder.header(name, value) + } + } chain.proceed(builder.build()) } @@ -53,12 +61,29 @@ public object DevSupportHttpClient { customHeaders[name] = value } + /** Add a custom header that is only applied to requests matching the given host. */ + @JvmStatic + public fun addRequestHeader(name: String, value: String, host: String) { + hostHeaders.getOrPut(host) { ConcurrentHashMap() }[name] = value + } + /** Remove a previously added custom header. */ @JvmStatic public fun removeRequestHeader(name: String) { customHeaders.remove(name) } + /** Remove a previously added host-specific custom header. */ + @JvmStatic + public fun removeRequestHeader(name: String, host: String) { + hostHeaders[host]?.let { headersForHost -> + headersForHost.remove(name) + if (headersForHost.isEmpty()) { + hostHeaders.remove(host) + } + } + } + /** * Returns the appropriate HTTP scheme ("http" or "https") for the given host. Uses "https" when * the host specifies port 443 explicitly (e.g. "example.com:443"). diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.kt index b753b07b892e..ebccd2bb7580 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/OkHttpClientProvider.kt @@ -8,6 +8,7 @@ package com.facebook.react.modules.network import android.content.Context +import com.facebook.react.devsupport.inspector.DevSupportHttpClient import java.io.File import java.util.concurrent.TimeUnit import okhttp3.Cache @@ -47,8 +48,10 @@ public object OkHttpClientProvider { @JvmStatic public fun createClientBuilder(): OkHttpClient.Builder { // No timeouts by default + // Use DevSupportHttpClient as base to inherit custom header interceptor val client: OkHttpClient.Builder = - OkHttpClient.Builder() + DevSupportHttpClient.httpClient + .newBuilder() .connectTimeout(0, TimeUnit.MILLISECONDS) .readTimeout(0, TimeUnit.MILLISECONDS) .writeTimeout(0, TimeUnit.MILLISECONDS) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.kt index 9c8d9cf1240e..76e73c4e3b13 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/websocket/WebSocketModule.kt @@ -19,6 +19,7 @@ import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.buildReadableMap import com.facebook.react.common.ReactConstants +import com.facebook.react.devsupport.inspector.DevSupportHttpClient import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.network.CustomClientBuilder import com.facebook.react.modules.network.ForwardingCookieHandler @@ -80,7 +81,8 @@ public class WebSocketModule(context: ReactApplicationContext) : ) { val id = socketID.toInt() val okHttpBuilder = - OkHttpClient.Builder() + DevSupportHttpClient.httpClient + .newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read @@ -199,8 +201,9 @@ public class WebSocketModule(context: ReactApplicationContext) : }, ) - // Trigger shutdown of the dispatcher's executor so this process can exit cleanly - client.dispatcher().executorService().shutdown() + // Note: Do NOT call client.dispatcher().executorService().shutdown() here. + // When building from a shared OkHttpClient (DevSupportHttpClient), the dispatcher + // is shared across all clients. Shutting it down would kill all connections. } override fun close(code: Double, reason: String?, socketID: Double) {