Skip to content

Commit

Permalink
Avoid re-encoding images when uploading local files (#31457)
Browse files Browse the repository at this point in the history
Summary:
Fixes #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: #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 (d0c8cb12f21604fd9730e275a52816d7fd00a826)-9164-4925-9025-1A76C0D9 (1946aee3d9696384d38890269ea705cafd472827)F0F5/data/Containers/Bundle/Application/B1E8A764-6221-4EA9-BE9A-2CB1699FD218 (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
  • Loading branch information
arthuralee authored and facebook-github-bot committed Jun 15, 2021
1 parent f12f0e6 commit f78526c
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Libraries/Network/RCTNetworking.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request
completionBlock:(RCTURLRequestCompletionBlock)completionBlock;

- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request handler:(id<RCTURLRequestHandler>)handler completionBlock:(RCTURLRequestCompletionBlock)completionBlock;

- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler;

- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
Expand Down
27 changes: 25 additions & 2 deletions Libraries/Network/RCTNetworking.mm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import <React/RCTUtils.h>

#import <React/RCTHTTPRequestHandler.h>
#import <React/RCTFileRequestHandler.h>

#import "RCTNetworkPlugins.h"

Expand Down Expand Up @@ -154,6 +155,7 @@ @implementation RCTNetworking
}

@synthesize methodQueue = _methodQueue;
@synthesize moduleRegistry = _moduleRegistry;

RCT_EXPORT_MODULE()

Expand Down Expand Up @@ -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<NSString *> *)supportedEvents
{
return @[@"didCompleteNetworkResponse",
Expand Down Expand Up @@ -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<RCTURLRequestHandler> 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);
});
Expand Down Expand Up @@ -676,6 +686,19 @@ - (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlo
return task;
}

- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request handler:(id<RCTURLRequestHandler>)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
Expand Down

0 comments on commit f78526c

Please sign in to comment.