Skip to content
Closed
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
9 changes: 8 additions & 1 deletion packages/react-native/Libraries/WebSocket/WebSocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ class WebSocket extends EventTarget {
);
this._socketId = nextWebSocketId++;
this._registerEvents();
NativeWebSocketModule.connect(url, protocols, {headers}, this._socketId);
const devToolsRequestId =
global.__NETWORK_REPORTER__?.createDevToolsRequestId();
NativeWebSocketModule.connect(
url,
protocols,
{headers, unstable_devToolsRequestId: devToolsRequestId},
this._socketId,
);
}

get binaryType(): ?BinaryType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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 <CFNetwork/CFNetwork.h>
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/**
* [Experimental] An interface for reporting WebSocket events to the modern
* debugger server.
*
* In a production (non dev or profiling) build, CDP reporting is disabled
* and all methods are a no-op.
*
* This is a helper class wrapping `facebook::react::NetworkReporter`.
*/
@interface RCTInspectorWebSocketReporter : NSObject

/**
* Report that a WebSocket connection is about to be created.
*
* Corresponds to `Network.webSocketCreated` in CDP.
*/
+ (void)reportWebSocketCreated:(nullable NSString *)requestId url:(NSURL *)url;

/**
* Report that a WebSocket handshake (HTTP upgrade) request is about to be
* sent, along with its final request headers.
*
* Corresponds to `Network.webSocketWillSendHandshakeRequest` in CDP.
*/
+ (void)reportWillSendHandshakeRequest:(nullable NSString *)requestId request:(NSURLRequest *)request;

/**
* Report that the WebSocket handshake response was received and the
* connection is established. `httpMessage` is the raw handshake response,
* e.g. `SRWebSocket.receivedHTTPHeaders`. If NULL or incomplete, a minimal
* `101 Switching Protocols` response is reported instead.
*
* Corresponds to `Network.webSocketHandshakeResponseReceived` in CDP.
*/
+ (void)reportHandshakeResponseReceived:(nullable NSString *)requestId
httpMessage:(nullable CFHTTPMessageRef)httpMessage;

/**
* Report a WebSocket message sent over an open connection. `message` must be
* an `NSString` (text message) or `NSData` (binary message).
*
* Corresponds to `Network.webSocketFrameSent` in CDP.
*/
+ (void)reportMessageSent:(nullable NSString *)requestId message:(nullable id)message;

/**
* Report a WebSocket message received over an open connection. `message`
* must be an `NSString` (text message) or `NSData` (binary message).
*
* Corresponds to `Network.webSocketFrameReceived` in CDP.
*/
+ (void)reportMessageReceived:(nullable NSString *)requestId message:(nullable id)message;

/**
* Report that a WebSocket connection was closed, whether cleanly or due to
* an error.
*
* Corresponds to `Network.webSocketClosed` in CDP.
*/
+ (void)reportWebSocketClosed:(nullable NSString *)requestId;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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 "RCTInspectorWebSocketReporter.h"

#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/networking/NetworkReporter.h>

using facebook::react::Headers;
using facebook::react::NetworkReporter;
using facebook::react::ReactNativeFeatureFlags;

namespace {

/**
* Convert an `NSString` to a `std::string`, mapping `nil` (and any string whose
* UTF-8 representation is unavailable) to an empty string.
*/
std::string toStdString(NSString *string)
{
const char *utf8 = string.UTF8String;
return utf8 != nullptr ? std::string(utf8) : std::string();
}

/**
* Returns whether WebSocket events should be reported for the given request
* ID. Reporting requires the `enableNetworkEventReporting` and
* `fuseboxWebSocketEventsEnabled` feature flags, and a connected CDP debugger
* with the Network domain enabled (dev or profiling builds only).
*/
BOOL isReportingEnabled(NSString *requestId)
{
return requestId != nil && ReactNativeFeatureFlags::enableNetworkEventReporting() &&
ReactNativeFeatureFlags::fuseboxWebSocketEventsEnabled() && NetworkReporter::getInstance().isDebuggingEnabled();
}

Headers convertNSDictionaryToHeaders(const NSDictionary<NSString *, NSString *> *headers)
{
Headers result;
for (NSString *key in headers) {
result[toStdString(key)] = toStdString(headers[key]);
}
return result;
}

/**
* Convert an `NSString` (text) or `NSData` (binary) WebSocket message to a
* CDP `payloadData` string — UTF-8 for text messages, base64 for binary.
* \returns Whether the message was a supported type, writing to the out
* params on success.
*/
bool convertMessageToPayloadData(id message, std::string &payloadData, bool &isBinary)
{
if ([message isKindOfClass:[NSString class]]) {
payloadData = toStdString((NSString *)message);
isBinary = false;
return true;
}

if ([message isKindOfClass:[NSData class]]) {
payloadData = toStdString([(NSData *)message base64EncodedStringWithOptions:0]);
isBinary = true;
return true;
}

return false;
}

} // namespace

@implementation RCTInspectorWebSocketReporter

+ (void)reportWebSocketCreated:(NSString *)requestId url:(NSURL *)url
{
if (!isReportingEnabled(requestId)) {
return;
}

NetworkReporter::getInstance().reportWebSocketCreated(toStdString(requestId), toStdString(url.absoluteString));
}

+ (void)reportWillSendHandshakeRequest:(NSString *)requestId request:(NSURLRequest *)request
{
if (!isReportingEnabled(requestId)) {
return;
}

NetworkReporter::getInstance().reportWebSocketWillSendHandshakeRequest(
toStdString(requestId), convertNSDictionaryToHeaders(request.allHTTPHeaderFields));
}

+ (void)reportHandshakeResponseReceived:(NSString *)requestId httpMessage:(nullable CFHTTPMessageRef)httpMessage
{
if (!isReportingEnabled(requestId)) {
return;
}

// Fall back to a minimal `101 Switching Protocols` response (guaranteed by
// RFC 6455 for an open connection) if the handshake response is unavailable.
uint16_t statusCode = 101;
Headers headers;

if (httpMessage != NULL && CFHTTPMessageIsHeaderComplete(httpMessage) != 0) {
statusCode = (uint16_t)CFHTTPMessageGetResponseStatusCode(httpMessage);
NSDictionary<NSString *, NSString *> *responseHeaders =
(NSDictionary<NSString *, NSString *> *)CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(httpMessage));
headers = convertNSDictionaryToHeaders(responseHeaders);
}

NetworkReporter::getInstance().reportWebSocketHandshakeResponseReceived(toStdString(requestId), statusCode, headers);
}

+ (void)reportMessageSent:(NSString *)requestId message:(id)message
{
if (!isReportingEnabled(requestId)) {
return;
}

std::string payloadData;
bool isBinary = false;
if (!convertMessageToPayloadData(message, payloadData, isBinary)) {
return;
}

NetworkReporter::getInstance().reportWebSocketMessageSent(toStdString(requestId), payloadData, isBinary);
}

+ (void)reportMessageReceived:(NSString *)requestId message:(id)message
{
if (!isReportingEnabled(requestId)) {
return;
}

std::string payloadData;
bool isBinary = false;
if (!convertMessageToPayloadData(message, payloadData, isBinary)) {
return;
}

NetworkReporter::getInstance().reportWebSocketMessageReceived(toStdString(requestId), payloadData, isBinary);
}

+ (void)reportWebSocketClosed:(NSString *)requestId
{
if (!isReportingEnabled(requestId)) {
return;
}

NetworkReporter::getInstance().reportWebSocketClosed(toStdString(requestId));
}

@end
42 changes: 40 additions & 2 deletions packages/react-native/React/CoreModules/RCTWebSocketModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
#import <SocketRocket/SRWebSocket.h>

#import "CoreModulesPlugins.h"
#import "RCTInspectorWebSocketReporter.h"

@interface SRWebSocket (React)

/**
* The CDP request ID used to report this connection's events to the modern
* debugger server, via `RCTInspectorWebSocketReporter`.
*/
@property (nonatomic, copy) NSString *inspectorRequestId;

@end

@implementation SRWebSocket (React)

Expand All @@ -29,6 +40,16 @@ - (void)setReactTag:(NSNumber *)reactTag
objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)inspectorRequestId
{
return objc_getAssociatedObject(self, _cmd);
}

- (void)setInspectorRequestId:(NSString *)inspectorRequestId
{
objc_setAssociatedObject(self, @selector(inspectorRequestId), inspectorRequestId, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

@interface RCTWebSocketModule () <SRWebSocketDelegate, NativeWebSocketModuleSpec>
Expand Down Expand Up @@ -131,16 +152,26 @@ - (void)invalidate
[webSocket setDelegateDispatchQueue:[self methodQueue]];
webSocket.delegate = self;
webSocket.reactTag = @(socketID);
// Prefer the DevTools request ID created by the JS caller, which carries the
// request initiator stack trace for CDP reporting.
NSString *devToolsRequestId = options.unstable_devToolsRequestId();
webSocket.inspectorRequestId = devToolsRequestId.length > 0 ? devToolsRequestId : [[NSUUID UUID] UUIDString];
if (!_sockets) {
_sockets = [NSMutableDictionary new];
}
_sockets[@(socketID)] = webSocket;

[RCTInspectorWebSocketReporter reportWebSocketCreated:webSocket.inspectorRequestId url:URL];
[RCTInspectorWebSocketReporter reportWillSendHandshakeRequest:webSocket.inspectorRequestId request:request];

[webSocket open];
}

RCT_EXPORT_METHOD(send : (NSString *)message forSocketID : (double)socketID)
{
[_sockets[@(socketID)] sendString:message error:nil];
SRWebSocket *webSocket = _sockets[@(socketID)];
[RCTInspectorWebSocketReporter reportMessageSent:webSocket.inspectorRequestId message:message];
[webSocket sendString:message error:nil];
}

RCT_EXPORT_METHOD(sendBinary : (NSString *)base64String forSocketID : (double)socketID)
Expand All @@ -150,7 +181,9 @@ - (void)invalidate

- (void)sendData:(NSData *)data forSocketID:(NSNumber *__nonnull)socketID
{
[_sockets[socketID] sendData:data error:nil];
SRWebSocket *webSocket = _sockets[socketID];
[RCTInspectorWebSocketReporter reportMessageSent:webSocket.inspectorRequestId message:data];
[webSocket sendData:data error:nil];
}

RCT_EXPORT_METHOD(ping : (double)socketID)
Expand Down Expand Up @@ -182,6 +215,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
if (!socketID) {
return;
}
[RCTInspectorWebSocketReporter reportMessageReceived:webSocket.inspectorRequestId message:message];
id contentHandler = _contentHandlers[socketID];
if (contentHandler) {
message = [contentHandler processWebsocketMessage:message forSocketID:socketID withType:&type];
Expand All @@ -203,6 +237,8 @@ - (void)webSocketDidOpen:(SRWebSocket *)webSocket
if (!socketID) {
return;
}
[RCTInspectorWebSocketReporter reportHandshakeResponseReceived:webSocket.inspectorRequestId
httpMessage:webSocket.receivedHTTPHeaders];
[self sendEventWithName:@"websocketOpen"
body:@{@"id" : socketID, @"protocol" : webSocket.protocol ? webSocket.protocol : @""}];
}
Expand All @@ -213,6 +249,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
if (!socketID) {
return;
}
[RCTInspectorWebSocketReporter reportWebSocketClosed:webSocket.inspectorRequestId];
_contentHandlers[socketID] = nil;
_sockets[socketID] = nil;
NSDictionary *body =
Expand All @@ -229,6 +266,7 @@ - (void)webSocket:(SRWebSocket *)webSocket
if (!socketID) {
return;
}
[RCTInspectorWebSocketReporter reportWebSocketClosed:webSocket.inspectorRequestId];
_contentHandlers[socketID] = nil;
_sockets[socketID] = nil;
[self sendEventWithName:@"websocketClosed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Pod::Spec.new do |s|
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
add_dependency(s, "React-networking", :framework_name => 'React_networking')

add_dependency(s, "React-RCTFBReactNativeSpec")
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<854bb2a573f2c69d495aa1721f903ab0>>
* @generated SignedSource<<df3f81d0083a8ebe9d0fbcb86e652405>>
*/

/**
Expand Down Expand Up @@ -390,6 +390,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun fuseboxScreenshotCaptureEnabled(): Boolean = accessor.fuseboxScreenshotCaptureEnabled()

/**
* Enable reporting of WebSocket network events (`Network.webSocket*` CDP events) to the React Native DevTools CDP backend. Requires `fuseboxNetworkInspectionEnabled`.
*/
@JvmStatic
public fun fuseboxWebSocketEventsEnabled(): Boolean = accessor.fuseboxWebSocketEventsEnabled()

/**
* When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. On iOS, this passes AnimatedProps directly through the delegate chain and applies them via cloneProps, avoiding the folly::dynamic round-trip.
*/
Expand Down
Loading
Loading