From fc803c238678344f25d7c5ee562cbcc64f96f9d9 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Wed, 7 Aug 2024 06:12:10 -0700 Subject: [PATCH] Implement HostTargetDelegate.networkRequest on iOS (#44846) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44846 Implement the `networkRequest` method of `jsinspector_modern::HostTargetDelegate` for iOS (bridge and bridgeless) to satisfy CDP `Network.loadNetworkResource` (etc) requests. Changelog: [iOS][Added] Debugger: Implement CDP methods for loading network resources through the debug target. Reviewed By: huntie Differential Revision: D54496969 --- packages/react-native/React/Base/RCTBridge.mm | 12 +- .../DevSupport/RCTInspectorNetworkHelper.h | 25 ++++ .../DevSupport/RCTInspectorNetworkHelper.mm | 115 ++++++++++++++++++ .../platform/ios/ReactCommon/RCTHost.mm | 12 +- 4 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.h create mode 100644 packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index 8183fbb9d55eeb..cc119a82b6bdc7 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -20,6 +20,7 @@ #import #import #import "RCTDevLoadingViewProtocol.h" +#import "RCTInspectorNetworkHelper.h" #import "RCTInspectorUtils.h" #import "RCTJSThread.h" #import "RCTLog.h" @@ -182,7 +183,9 @@ void RCTUIManagerSetDispatchAccessibilityManagerInitOntoMain(BOOL enabled) class RCTBridgeHostTargetDelegate : public facebook::react::jsinspector_modern::HostTargetDelegate { public: RCTBridgeHostTargetDelegate(RCTBridge *bridge) - : bridge_(bridge), pauseOverlayController_([[RCTPausedInDebuggerOverlayController alloc] init]) + : bridge_(bridge), + pauseOverlayController_([[RCTPausedInDebuggerOverlayController alloc] init]), + networkHelper_([[RCTInspectorNetworkHelper alloc] init]) { } @@ -229,9 +232,16 @@ void onSetPausedInDebuggerMessage(const OverlaySetPausedInDebuggerMessageRequest } } + void loadNetworkResource(const RCTInspectorLoadNetworkResourceRequest ¶ms, RCTInspectorNetworkExecutor executor) + override + { + [networkHelper_ loadNetworkResourceWithParams:params executor:executor]; + } + private: __weak RCTBridge *bridge_; RCTPausedInDebuggerOverlayController *pauseOverlayController_; + RCTInspectorNetworkHelper *networkHelper_; }; @interface RCTBridge () diff --git a/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.h b/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.h new file mode 100644 index 00000000000000..738e20b2eb38d9 --- /dev/null +++ b/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.h @@ -0,0 +1,25 @@ +/* + * 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 + +#import + +typedef facebook::react::jsinspector_modern::NetworkRequestListener RCTInspectorNetworkListener; + +typedef facebook::react::jsinspector_modern::ScopedExecutor RCTInspectorNetworkExecutor; + +typedef facebook::react::jsinspector_modern::LoadNetworkResourceRequest RCTInspectorLoadNetworkResourceRequest; + +/** + * A helper class that wraps around NSURLSession to make network requests. + */ +@interface RCTInspectorNetworkHelper : NSObject +- (instancetype)init; +- (void)loadNetworkResourceWithParams:(const RCTInspectorLoadNetworkResourceRequest &)params + executor:(RCTInspectorNetworkExecutor)executor; +@end diff --git a/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm new file mode 100644 index 00000000000000..538d9767e1d2f4 --- /dev/null +++ b/packages/react-native/React/DevSupport/RCTInspectorNetworkHelper.mm @@ -0,0 +1,115 @@ +/* + * 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 "RCTInspectorNetworkHelper.h" +#import + +typedef void (^ListenerBlock)(RCTInspectorNetworkListener *); + +@interface RCTInspectorNetworkHelper () +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, strong) NSMutableDictionary *executorsByTaskId; +- (void)withListenerForTask:(NSURLSessionTask *)task execute:(ListenerBlock)block; +@end + +@implementation RCTInspectorNetworkHelper + +- (instancetype)init +{ + self = [super init]; + if (self) { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; + self.executorsByTaskId = [NSMutableDictionary new]; + } + return self; +} + +- (void)loadNetworkResourceWithParams:(const RCTInspectorLoadNetworkResourceRequest &)params + executor:(RCTInspectorNetworkExecutor)executor +{ + NSString *urlString = [NSString stringWithCString:params.url.c_str() encoding:NSUTF8StringEncoding]; + NSURL *url = [NSURL URLWithString:urlString]; + auto executorBlock = ^(ListenerBlock func) { + executor([=](RCTInspectorNetworkListener &listener) { func(&listener); }); + }; + + if (url == nil) { + executorBlock(^(RCTInspectorNetworkListener *listener) { + listener->onError([NSString stringWithFormat:@"Not a valid URL: %@", urlString].UTF8String); + }); + return; + } + + NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url]; + [urlRequest setHTTPMethod:@"GET"]; + NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:urlRequest]; + __weak NSURLSessionDataTask *weakDataTask = dataTask; + + executorBlock(^(RCTInspectorNetworkListener *listener) { + listener->setCancelFunction([weakDataTask]() { [weakDataTask cancel]; }); + }); + + // Store the executor as a block per task. + self.executorsByTaskId[@(dataTask.taskIdentifier)] = executorBlock; + + [dataTask resume]; +} + +- (void)withListenerForTask:(NSURLSessionTask *)task execute:(ListenerBlock)block +{ + void (^executor)(ListenerBlock) = self.executorsByTaskId[@(task.taskIdentifier)]; + if (executor) { + executor(block); + } +} + +#pragma mark - NSURLSessionDataDelegate + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler +{ + auto callbackWithHeadersOrError = (^(RCTInspectorNetworkListener *listener) { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + std::map headersMap; + for (NSString *key in httpResponse.allHeaderFields) { + headersMap[[key UTF8String]] = [[httpResponse.allHeaderFields objectForKey:key] UTF8String]; + } + completionHandler(NSURLSessionResponseAllow); + listener->onHeaders(httpResponse.statusCode, headersMap); + } else { + listener->onError("Unsupported response type"); + completionHandler(NSURLSessionResponseCancel); + } + }); + [self withListenerForTask:dataTask execute:callbackWithHeadersOrError]; +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data +{ + auto callbackWithData = ^(RCTInspectorNetworkListener *listener) { + listener->onData(std::string_view(static_cast(data.bytes), data.length)); + }; + [self withListenerForTask:dataTask execute:callbackWithData]; +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error +{ + auto callbackWithCompletionOrError = ^(RCTInspectorNetworkListener *listener) { + if (error != nil) { + listener->onError(error.localizedDescription.UTF8String); + } else { + listener->onCompletion(); + } + }; + [self withListenerForTask:task execute:callbackWithCompletionOrError]; +} + +@end diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm index c738e94b778105..86598b49c981f3 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm @@ -13,6 +13,7 @@ #import #import #import +#import #import #import #import @@ -37,7 +38,9 @@ @interface RCTHost () class RCTHostHostTargetDelegate : public facebook::react::jsinspector_modern::HostTargetDelegate { public: RCTHostHostTargetDelegate(RCTHost *host) - : host_(host), pauseOverlayController_([[RCTPausedInDebuggerOverlayController alloc] init]) + : host_(host), + pauseOverlayController_([[RCTPausedInDebuggerOverlayController alloc] init]), + networkHelper_([[RCTInspectorNetworkHelper alloc] init]) { } @@ -84,9 +87,16 @@ void onSetPausedInDebuggerMessage(const OverlaySetPausedInDebuggerMessageRequest } } + void loadNetworkResource(const RCTInspectorLoadNetworkResourceRequest ¶ms, RCTInspectorNetworkExecutor executor) + override + { + [networkHelper_ loadNetworkResourceWithParams:params executor:executor]; + } + private: __weak RCTHost *host_; RCTPausedInDebuggerOverlayController *pauseOverlayController_; + RCTInspectorNetworkHelper *networkHelper_; }; @implementation RCTHost {