diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml index 5173a16fd741..878444dcfbf0 100644 --- a/.github/actions/build-android/action.yml +++ b/.github/actions/build-android/action.yml @@ -26,7 +26,9 @@ runs: # already set from the 'create release' commits on the release branch. # For testing RC.0, though, the version has not been set yet. In that case, we are on Stable branch and # it is the only case when the version is still 1000.0.0 - if: ${{ !endsWith(github.ref_name, '-stable') || endsWith(github.ref_name, '-stable') && steps.read-rn-version.outputs.rn-version == '1000.0.0' }} + # We also skip this when the PR targets a stable branch (github.base_ref ends with '-stable'), + # since the version is already set on the stable branch. + if: ${{ !endsWith(github.ref_name, '-stable') && !endsWith(github.base_ref || '', '-stable') || endsWith(github.ref_name, '-stable') && steps.read-rn-version.outputs.rn-version == '1000.0.0' }} shell: bash run: node ./scripts/releases/set-rn-artifacts-version.js --build-type ${{ inputs.release-type }} - name: Setup gradle diff --git a/packages/react-native/Libraries/Core/setUpReactDevTools.js b/packages/react-native/Libraries/Core/setUpReactDevTools.js index 9bdf66b30184..2c80b23393ad 100644 --- a/packages/react-native/Libraries/Core/setUpReactDevTools.js +++ b/packages/react-native/Libraries/Core/setUpReactDevTools.js @@ -146,17 +146,34 @@ 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; + } + } 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.h b/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.h index ee7fe8be44f7..c3a27e15a045 100644 --- a/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.h +++ b/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.h @@ -14,6 +14,15 @@ typedef NSURLSessionConfiguration * (^NSURLSessionConfigurationProvider)(void); * app. */ RCT_EXTERN void RCTSetCustomNSURLSessionConfigurationProvider(NSURLSessionConfigurationProvider /*provider*/); + +typedef NSURLRequest *_Nullable (^RCTHTTPRequestInterceptor)(NSURLRequest *request); +/** + * The block provided via this function can inspect/modify HTTP requests before + * they are sent. Return a modified request to override, or nil to use the + * original request unchanged. + */ +RCT_EXTERN void RCTSetCustomHTTPRequestInterceptor(RCTHTTPRequestInterceptor /*interceptor*/); + /** * This is the default RCTURLRequestHandler implementation for HTTP requests. */ diff --git a/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm b/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm index 0303970a2e47..61de2a064155 100644 --- a/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm +++ b/packages/react-native/Libraries/Network/RCTHTTPRequestHandler.mm @@ -25,6 +25,13 @@ void RCTSetCustomNSURLSessionConfigurationProvider(NSURLSessionConfigurationProv urlSessionConfigurationProvider = provider; } +static RCTHTTPRequestInterceptor httpRequestInterceptor; + +void RCTSetCustomHTTPRequestInterceptor(RCTHTTPRequestInterceptor interceptor) +{ + httpRequestInterceptor = interceptor; +} + @implementation RCTHTTPRequestHandler { NSMapTable *_delegates; NSURLSession *_session; @@ -99,7 +106,14 @@ - (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request withDelegate:(id #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.h b/packages/react-native/React/Base/RCTMultipartDataTask.h index 25a73fdaf1af..eba097484e20 100644 --- a/packages/react-native/React/Base/RCTMultipartDataTask.h +++ b/packages/react-native/React/Base/RCTMultipartDataTask.h @@ -7,6 +7,7 @@ #import +#import #import typedef void (^RCTMultipartDataTaskCallback)( @@ -16,6 +17,14 @@ typedef void (^RCTMultipartDataTaskCallback)( NSError *error, BOOL done); +typedef NSURLRequest * _Nullable (^RCTMultipartDataTaskRequestInterceptor)(NSURLRequest *request); +/** + * The block provided via this function can inspect/modify multipart data task + * requests before they are sent. Return a modified request to override, or nil + * to use the original request unchanged. + */ +RCT_EXTERN void RCTSetCustomMultipartDataTaskRequestInterceptor(RCTMultipartDataTaskRequestInterceptor /*interceptor*/); + @interface RCTMultipartDataTask : NSObject - (instancetype)initWithURL:(NSURL *)url diff --git a/packages/react-native/React/Base/RCTMultipartDataTask.m b/packages/react-native/React/Base/RCTMultipartDataTask.m index 3ac07683eb93..b68dee6b4a1f 100644 --- a/packages/react-native/React/Base/RCTMultipartDataTask.m +++ b/packages/react-native/React/Base/RCTMultipartDataTask.m @@ -7,6 +7,13 @@ #import "RCTMultipartDataTask.h" +static RCTMultipartDataTaskRequestInterceptor multipartRequestInterceptor; + +void RCTSetCustomMultipartDataTaskRequestInterceptor(RCTMultipartDataTaskRequestInterceptor interceptor) +{ + multipartRequestInterceptor = interceptor; +} + @interface RCTMultipartDataTask () @end @@ -40,7 +47,15 @@ - (void)startTask delegateQueue:nil]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url]; [request addValue:@"multipart/mixed" forHTTPHeaderField:@"Accept"]; - NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request]; + NSURLRequest *finalRequest = request; + if (multipartRequestInterceptor != nil) { + NSURLRequest *intercepted = multipartRequestInterceptor(request); + if (intercepted != nil) { + finalRequest = intercepted; + } + } + NSLog(@"[RCTMultipartDataTask] %@ %@", finalRequest.HTTPMethod ?: @"GET", finalRequest.URL.absoluteString); + NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:finalRequest]; [dataTask resume]; [session finishTasksAndInvalidate]; } diff --git a/packages/react-native/React/CoreModules/RCTWebSocketModule.h b/packages/react-native/React/CoreModules/RCTWebSocketModule.h index f92bfc42e3ae..0deba70671ff 100644 --- a/packages/react-native/React/CoreModules/RCTWebSocketModule.h +++ b/packages/react-native/React/CoreModules/RCTWebSocketModule.h @@ -18,6 +18,12 @@ NS_ASSUME_NONNULL_BEGIN @end +@class SRWebSocket; + +typedef SRWebSocket * (^SRWebSocketProvider)(NSURLRequest *request); + +RCT_EXTERN void RCTSetCustomSRWebSocketProvider(SRWebSocketProvider provider); + @interface RCTWebSocketModule : RCTEventEmitter // Register a custom handler for a specific websocket. The handler will be strongly held by the WebSocketModule. diff --git a/packages/react-native/React/CoreModules/RCTWebSocketModule.mm b/packages/react-native/React/CoreModules/RCTWebSocketModule.mm index e29d76a76a23..5d95b90bca81 100644 --- a/packages/react-native/React/CoreModules/RCTWebSocketModule.mm +++ b/packages/react-native/React/CoreModules/RCTWebSocketModule.mm @@ -34,6 +34,13 @@ @interface RCTWebSocketModule () *_sockets; NSMutableDictionary> *_contentHandlers; @@ -88,7 +95,13 @@ - (void)invalidate }]; } - SRWebSocket *webSocket = [[SRWebSocket alloc] initWithURLRequest:request protocols:protocols]; + SRWebSocket *webSocket; + if (srWebSocketProvider != nullptr) { + webSocket = srWebSocketProvider(request); + } + if (webSocket == nil) { + webSocket = [[SRWebSocket alloc] initWithURLRequest:request protocols:protocols]; + } [webSocket setDelegateDispatchQueue:[self methodQueue]]; webSocket.delegate = self; webSocket.reactTag = @(socketID); diff --git a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm index 9230dd3b3322..e2ee27e2d881 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -14,33 +14,39 @@ #import #import +#import #import #import static NSString *const kDebuggerMsgDisable = @"{ \"id\":1,\"method\":\"Debugger.disable\" }"; +static const int kDefaultMetroPort = 8081; 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:@"%@:%d", host, kDefaultMetroPort]; } static NSString *getSHA256(NSString *string) @@ -111,13 +117,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] != nullptr ? [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 @@ -149,11 +157,14 @@ + (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] != nullptr ? [bundleURL scheme] : @"http"; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@/open-debugger?device=%@", + scheme, getServerHost(bundleURL), 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