Support CDP response previews for chunked data (#52331)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52331

Updates the iOS inputs to `NetworkReporter` to support incremental string data HTTP responses (`Transfer-Encoding: chunked`).

This means that incremental responses, such as Metro bundle requests, can be displayed as previews in React Native DevTools.

Changelog: [Internal]

Reviewed By: hoxyq

Differential Revision: D77457109

fbshipit-source-id: 00a622dbac97c38e07c67b5ee3661c8d586f6fe1
This commit is contained in:
Alex Hunt
2025-07-03 05:01:57 -07:00
committed by Facebook GitHub Bot
parent 94c97db4da
commit 68342a4d12
3 changed files with 86 additions and 4 deletions
@@ -52,9 +52,27 @@
*/
+ (void)reportResponseEnd:(NSNumber *)requestId encodedDataLength:(int)encodedDataLength;
/**
* Report when a network request has failed.
*
* - Corresponds to `Network.loadingFailed` in CDP.
*/
+ (void)reportRequestFailed:(NSNumber *)requestId;
/**
* Store response body preview. This is an optional reporting method, and is a
* no-op if CDP debugging is disabled.
*/
+ (void)maybeStoreResponseBody:(NSNumber *)requestId data:(NSData *)data base64Encoded:(bool)base64Encoded;
/**
* Incrementally store a response body preview, when a string response is
* received in chunks. Buffered contents will be flushed to `NetworkReporter`
* with `reportResponseEnd`.
*
* As with `maybeStoreResponseBody`, calling this method is optional and a
* no-op if CDP debugging is disabled.
*/
+ (void)maybeStoreResponseBodyIncremental:(NSNumber *)requestId data:(NSString *)data;
@end
@@ -45,6 +45,13 @@ std::string convertRequestBodyToStringTruncated(NSURLRequest *request)
} // namespace
#endif
#ifdef REACT_NATIVE_DEBUGGER_ENABLED
// Dictionary to buffer incremental response bodies (CDP debugging active only)
static const NSMutableDictionary<NSNumber *, NSMutableString *> *responseBuffers = nil;
#endif
@implementation RCTInspectorNetworkReporter {
}
@@ -86,6 +93,31 @@ std::string convertRequestBodyToStringTruncated(NSURLRequest *request)
+ (void)reportResponseEnd:(NSNumber *)requestId encodedDataLength:(int)encodedDataLength
{
NetworkReporter::getInstance().reportResponseEnd(requestId.stringValue.UTF8String, encodedDataLength);
#ifdef REACT_NATIVE_DEBUGGER_ENABLED
// Debug build: Check for buffered response body and flush to NetworkReporter
if (responseBuffers != nullptr) {
NSMutableString *buffer = responseBuffers[requestId];
if (buffer != nullptr) {
if (buffer.length > 0) {
NetworkReporter::getInstance().storeResponseBody(
requestId.stringValue.UTF8String, RCTStringViewFromNSString(buffer), false);
}
[responseBuffers removeObjectForKey:requestId];
}
}
#endif
}
// TODO(T218584924): Implement and report to NetworkReporter
+ (void)reportRequestFailed:(NSNumber *)requestId
{
#ifdef REACT_NATIVE_DEBUGGER_ENABLED
// Debug build: Clear buffer for request
if (responseBuffers != nullptr) {
[responseBuffers removeObjectForKey:requestId];
}
#endif
}
+ (void)maybeStoreResponseBody:(NSNumber *)requestId data:(id)data base64Encoded:(bool)base64Encoded
@@ -115,4 +147,29 @@ std::string convertRequestBodyToStringTruncated(NSURLRequest *request)
}
#endif
}
+ (void)maybeStoreResponseBodyIncremental:(NSNumber *)requestId data:(NSString *)data
{
#ifdef REACT_NATIVE_DEBUGGER_ENABLED
// Debug build: Buffer incremental response body contents
auto &networkReporter = NetworkReporter::getInstance();
if (!networkReporter.isDebuggingEnabled()) {
return;
}
if (responseBuffers == nullptr) {
responseBuffers = [NSMutableDictionary dictionary];
}
// Get or create buffer for this requestId
NSMutableString *buffer = responseBuffers[requestId];
if (buffer == nullptr) {
buffer = [NSMutableString string];
responseBuffers[requestId] = buffer;
}
[buffer appendString:data];
#endif
}
@end
@@ -627,9 +627,9 @@ RCT_EXPORT_MODULE()
incrementalDataBlock = ^(NSData *data, int64_t progress, int64_t total) {
NSUInteger initialCarryLength = incrementalDataCarry.length;
NSString *responseString = [RCTNetworking decodeTextData:data
fromResponse:task.response
withCarryData:incrementalDataCarry];
id responseString = [RCTNetworking decodeTextData:data
fromResponse:task.response
withCarryData:incrementalDataCarry];
if (!responseString) {
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
return;
@@ -643,6 +643,9 @@ RCT_EXPORT_MODULE()
@(total)
];
if (facebook::react::ReactNativeFeatureFlags::enableNetworkEventReporting()) {
[RCTInspectorNetworkReporter maybeStoreResponseBodyIncremental:task.requestID data:responseString];
}
[weakSelf sendEventWithName:@"didReceiveNetworkIncrementalData" body:responseJSON];
};
} else {
@@ -668,7 +671,11 @@ RCT_EXPORT_MODULE()
@[ task.requestID, RCTNullIfNil(error.localizedDescription), error.code == kCFURLErrorTimedOut ? @YES : @NO ];
if (facebook::react::ReactNativeFeatureFlags::enableNetworkEventReporting()) {
[RCTInspectorNetworkReporter reportResponseEnd:task.requestID encodedDataLength:data.length];
if (error != nullptr) {
[RCTInspectorNetworkReporter reportRequestFailed:task.requestID];
} else {
[RCTInspectorNetworkReporter reportResponseEnd:task.requestID encodedDataLength:data.length];
}
}
[strongSelf sendEventWithName:@"didCompleteNetworkResponse" body:responseJSON];
[strongSelf->_tasksByRequestID removeObjectForKey:task.requestID];