Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions packages/react-native/Libraries/Core/setUpReactDevTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#import <mutex>

#import <React/RCTDevSupportHttpHeaders.h>
#import <React/RCTNetworking.h>
#import <ReactCommon/RCTTurboModule.h>

Expand Down Expand Up @@ -99,7 +100,9 @@ - (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request withDelegate:(id<R
valueOptions:NSPointerFunctionsStrongMemory
capacity:0];
}
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[[RCTDevSupportHttpHeaders sharedInstance] applyHeadersToRequest:mutableRequest];
NSURLSessionDataTask *task = [_session dataTaskWithRequest:mutableRequest];
[_delegates setObject:delegate forKey:task];
[task resume];
return task;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/React/Base/RCTDevSupportHttpHeaders.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
+ (instancetype)sharedInstance;

- (void)addRequestHeader:(NSString *)name value:(NSString *)value;
- (void)addRequestHeader:(NSString *)name value:(NSString *)value forHost:(NSString *)host;
- (void)removeRequestHeader:(NSString *)name;
- (void)removeRequestHeader:(NSString *)name forHost:(NSString *)host;
- (NSDictionary<NSString *, NSString *> *)allHeaders;
- (void)applyHeadersToRequest:(NSMutableURLRequest *)request;

Expand Down
46 changes: 44 additions & 2 deletions packages/react-native/React/Base/RCTDevSupportHttpHeaders.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

@implementation RCTDevSupportHttpHeaders {
NSMutableDictionary<NSString *, NSString *> *_headers;
NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSString *> *> *_hostHeaders;
dispatch_queue_t _queue;
}

Expand All @@ -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;
Expand All @@ -38,13 +40,38 @@ - (void)addRequestHeader:(NSString *)name value:(NSString *)value
});
}

- (void)addRequestHeader:(NSString *)name value:(NSString *)value forHost:(NSString *)host
{
dispatch_sync(_queue, ^{
NSMutableDictionary<NSString *, NSString *> *headersForHost = self->_hostHeaders[host];
if (headersForHost == nil) {
headersForHost = [NSMutableDictionary new];
self->_hostHeaders[host] = headersForHost;
}
headersForHost[name] = value;
});
}

- (void)removeRequestHeader:(NSString *)name
{
dispatch_sync(_queue, ^{
[self->_headers removeObjectForKey:name];
});
}

- (void)removeRequestHeader:(NSString *)name forHost:(NSString *)host
{
dispatch_sync(_queue, ^{
NSMutableDictionary<NSString *, NSString *> *headersForHost = self->_hostHeaders[host];
if (headersForHost != nil) {
[headersForHost removeObjectForKey:name];
if (headersForHost.count == 0) {
[self->_hostHeaders removeObjectForKey:host];
}
}
});
}

- (NSDictionary<NSString *, NSString *> *)allHeaders
{
__block NSDictionary<NSString *, NSString *> *snapshot;
Expand All @@ -56,8 +83,23 @@ - (void)removeRequestHeader:(NSString *)name

- (void)applyHeadersToRequest:(NSMutableURLRequest *)request
{
NSDictionary<NSString *, NSString *> *headers = [self allHeaders];
[headers enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) {
__block NSDictionary<NSString *, NSString *> *globalHeaders;
__block NSDictionary<NSString *, NSString *> *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];
}];
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native/React/CoreModules/RCTWebSocketModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTAssert.h>
#import <React/RCTConvert.h>
#import <React/RCTDevSupportHttpHeaders.h>
#import <React/RCTUtils.h>
#import <SocketRocket/SRWebSocket.h>

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@ import okhttp3.OkHttpClient
*/
public object DevSupportHttpClient {
private val customHeaders = ConcurrentHashMap<String, String>()
private val hostHeaders = ConcurrentHashMap<String, ConcurrentHashMap<String, String>>()

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())
}

Expand All @@ -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").
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading