From f78526ce3d4004eb4bf8ca5178ca7e2c1c9abc1a Mon Sep 17 00:00:00 2001 From: Arthur Lee Date: Mon, 14 Jun 2021 22:51:42 -0700 Subject: [PATCH] Avoid re-encoding images when uploading local files (#31457) Summary: Fixes https://github.com/facebook/react-native/issues/27099 When you upload a local file using XHR + the `FormData` API, RN uses `RCTNetworkTask` to retrieve the image file data from the local filesystem (request URL is a file:// URL) ([code pointer](https://github.com/facebook/react-native/blob/master/Libraries/Network/RCTNetworking.mm#L398)). As a result, if you are uploading a local image file that is in the app's directory `RCTNetworkTask` will end up using `RCTLocalAssetImageLoader` to load the image, which reads the image into a `UIImage` and then re-encodes it using `UIImageJPEGRepresentation` with a compression quality of 1.0, which is the higest ([code pointer](https://github.com/facebook/react-native/blob/4c5182c1cc8bafb15490adf602c87cb5bf289ffd/Libraries/Image/RCTImageLoader.mm#L1114)). Not only is this unnecessary, it ends up inflating the size of the jpg if it had been previously compressed to a lower quality. With this PR, this issue is fixed by forcing the `RCTFileRequestHandler` to be used when retrieving local files for upload, regardless of whether they are images or not. As a result, any file to be uploaded gets read into `NSData` which is the format needed when appending to the multipart body. I considered fixing this by modifying the behavior of how the handlers were chosen, but this felt like a safer fix since it will be scoped to just uploads and wont affect image fetching. ## Changelog [iOS] [Fixed] - Avoid re-encoding images when uploading local files Pull Request resolved: https://github.com/facebook/react-native/pull/31457 Test Plan: The repro for this is a bit troublesome, especially because this issue doesn't repro in RNTester. There is [some code](https://github.com/facebook/react-native/blob/master/packages/rn-tester/RNTester/AppDelegate.mm#L220) that is to be overriding the handlers that will be used, excluding the `RCTImageLoader`. I had to repro this in a fresh new RN app. 1. Create a blank RN app 2. Put an image in the folder of the app's install location. This would be similar to where files might be placed after an app downloads or captures an image. 3. Set up a quick express server that accepts multipart form uploads and stores the files 4. Trigger an upload via react native ``` const data = new FormData(); data.append('image', { uri: '/Users/arthur.lee/Library/Developer/CoreSimulator/Devices/46CDD981 (https://github.com/facebook/react-native/commit/d0c8cb12f21604fd9730e275a52816d7fd00a826)-9164-4925-9025-1A76C0D9 (https://github.com/facebook/react-native/commit/1946aee3d9696384d38890269ea705cafd472827)F0F5/data/Containers/Bundle/Application/B1E8A764-6221-4EA9-BE9A-2CB1699FD218 (https://github.com/facebook/react-native/commit/1c92b1cff623ea3f3b78238b146ab001626ef305)/test.app/test.bundle/compressed.jpg', type: 'image/jpeg', name: 'image.jpeg', }); fetch(`http://localhost:3000/upload`, { method: 'POST', headers: {'Content-Type': 'multipart/form-data'}, body: data, }).then(console.log); ``` 5. Trigger the upload with and without this patch Original file: ``` $ ls -lh total 448 -rw-r--r-- 1 arthur.lee staff 223K Apr 29 17:08 compressed.jpg ``` Uploaded file (with and without patch): ``` $ ls -lh total 1624 -rw-r--r--@ 1 arthur.lee staff 584K Apr 29 17:11 image-nopatch.jpeg -rw-r--r--@ 1 arthur.lee staff 223K Apr 29 17:20 image-withpatch.jpeg ``` Would appreciate pointers on whether this needs to be tested more extensively Reviewed By: yungsters Differential Revision: D28630805 Pulled By: PeteTheHeat fbshipit-source-id: 606a6091fa3e817966548c5eb84b19cb8b9abb1c --- Libraries/Network/RCTNetworking.h | 2 ++ Libraries/Network/RCTNetworking.mm | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Libraries/Network/RCTNetworking.h b/Libraries/Network/RCTNetworking.h index 2068f32b803038..358ab52b6d1206 100644 --- a/Libraries/Network/RCTNetworking.h +++ b/Libraries/Network/RCTNetworking.h @@ -45,6 +45,8 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlock:(RCTURLRequestCompletionBlock)completionBlock; +- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request handler:(id)handler completionBlock:(RCTURLRequestCompletionBlock)completionBlock; + - (void)addRequestHandler:(id)handler; - (void)addResponseHandler:(id)handler; diff --git a/Libraries/Network/RCTNetworking.mm b/Libraries/Network/RCTNetworking.mm index 22ca45b0fa3ff4..11e569cebb3c69 100644 --- a/Libraries/Network/RCTNetworking.mm +++ b/Libraries/Network/RCTNetworking.mm @@ -17,6 +17,7 @@ #import #import +#import #import "RCTNetworkPlugins.h" @@ -154,6 +155,7 @@ @implementation RCTNetworking } @synthesize methodQueue = _methodQueue; +@synthesize moduleRegistry = _moduleRegistry; RCT_EXPORT_MODULE() @@ -188,6 +190,14 @@ - (void)invalidate _responseHandlers = nil; } +// TODO (T93136931) - Investigate why this is needed. This setter shouldn't be +// necessary, since moduleRegistry is a property on RCTEventEmitter (which this +// class inherits from). +- (void)setModuleRegistry:(RCTModuleRegistry *)moduleRegistry +{ + _moduleRegistry = moduleRegistry; +} + - (NSArray *)supportedEvents { return @[@"didCompleteNetworkResponse", @@ -393,9 +403,9 @@ - (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(nullable NSDictionary } NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]]; if (request) { - __block RCTURLRequestCancellationBlock cancellationBlock = nil; - RCTNetworkTask *task = [self networkTaskWithRequest:request completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) { + id handler = [self.moduleRegistry moduleForName:"FileRequestHandler"]; + RCTNetworkTask *task = [self networkTaskWithRequest:request handler:handler completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) { dispatch_async(self->_methodQueue, ^{ cancellationBlock = callback(error, data ? @{@"body": data, @"contentType": RCTNullIfNil(response.MIMEType)} : nil); }); @@ -676,6 +686,19 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlo return task; } +- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request handler:(id)handler completionBlock:(RCTURLRequestCompletionBlock)completionBlock +{ + if (!handler) { + // specified handler is nil, fall back to generic method + return [self networkTaskWithRequest:request completionBlock:completionBlock]; + } + RCTNetworkTask *task = [[RCTNetworkTask alloc] initWithRequest:request + handler:handler + callbackQueue:_methodQueue]; + task.completionBlock = completionBlock; + return task; +} + #pragma mark - JS API RCT_EXPORT_METHOD(sendRequest:(JS::NativeNetworkingIOS::SpecSendRequestQuery &)query