From 68342a4d12867f717428dd600c83feebdc4a847b Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 3 Jul 2025 05:01:57 -0700 Subject: [PATCH] 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 --- .../Network/RCTInspectorNetworkReporter.h | 18 ++++++ .../Network/RCTInspectorNetworkReporter.mm | 57 +++++++++++++++++++ .../Libraries/Network/RCTNetworking.mm | 15 +++-- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h index 800ea99a765..728ba464db4 100644 --- a/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h +++ b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.h @@ -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 diff --git a/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm index 8abe4fe2e77..f0ccc6f3365 100644 --- a/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm +++ b/packages/react-native/Libraries/Network/RCTInspectorNetworkReporter.mm @@ -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 *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 diff --git a/packages/react-native/Libraries/Network/RCTNetworking.mm b/packages/react-native/Libraries/Network/RCTNetworking.mm index a92a2b418af..ae478eb7d22 100644 --- a/packages/react-native/Libraries/Network/RCTNetworking.mm +++ b/packages/react-native/Libraries/Network/RCTNetworking.mm @@ -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];