Skip to content

[iOS] Uploading an image will change the file size of the image #45962

Open

Description

@carverhong

Description

I uploaded a 2M image and the server received a 6M image. After debugging, the form data sent by the RCTNetworking became larger.

Steps to reproduce

  1. Clone the reproducer from https://github.com/carverhong/react-native-reproducer-ios-image-upload.git.

  2. Install dependency and prepare the ios project.

cd react-native-reproducer-ios-image-upload
npm install
bundle install
cd ios
bundle exec pod install

After the installation is complete, the RCTNetworking log patch is automatically added for debugging.

企业微信截图_42a75d24-fd86-4f7a-873f-933102ef4bcc
  1. Start Metro.
npm start
  1. Open HelloWorld.xcworkspace with XCode and run the APP.
  2. Use the RCTNetworking keyword to filter logs.
企业微信截图_33a594cf-1b54-42c5-ab8f-69b3b4484d01

React Native Version

0.72.15

Affected Platforms

Runtime - iOS

Output of npx react-native info

System:
  OS: macOS 14.4.1
  CPU: (8) arm64 Apple M3
  Memory: 87.55 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 18.19.0
    path: ~/tools/nodejs/node-v18.19.0-darwin-arm64/bin/node
  Yarn: Not Found
  npm:
    version: 10.2.3
    path: ~/tools/nodejs/node-v18.19.0-darwin-arm64/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods: Not Found
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 23.4
      - iOS 17.4
      - macOS 14.4
      - tvOS 17.4
      - visionOS 1.1
      - watchOS 10.4
  Android SDK: Not Found
IDEs:
  Android Studio: 2023.1 AI-231.9392.1.2311.11076708
  Xcode:
    version: 15.3/15E204a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.11
    path: /usr/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.72.15
    wanted: 0.72.15
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: false

Stacktrace or Logs

[RCTNetworking processDataForHTTPQuery] file:///Users/carverhong/Library/Developer/CoreSimulator/Devices/E094052D-91B7-493F-A39A-72F089B8937D/data/Containers/Bundle/Application/8BFC2E5C-87BD-4295-A8F0-F04272BC374A/HelloWorld.app/20240808144728-0.jpg rawDataSize=2017293
[RCTNetworking handleResult] multipartDataSize=6151776

Reproducer

https://github.com/carverhong/react-native-reproducer-ios-image-upload.git

Screenshots and Videos

No response

Activity

  1. react-native-bot commented on Aug 9, 2024

    @react-native-bot
    Collaborator
    ⚠️ Missing Reproducible Example
    ℹ️ We could not detect a reproducible example in your issue report. Please provide either:
    • If your bug is UI related: a Snack
    • If your bug is build/update related: use our Reproducer Template. A reproducer needs to be in a GitHub repository under your username.
  2. react-native-bot commented on Aug 9, 2024

    @react-native-bot
    Collaborator
    ⚠️ Missing Reproducible Example
    ℹ️ We could not detect a reproducible example in your issue report. Please provide either:
  3. cortinico commented on Aug 9, 2024

    @cortinico
    Contributor

    Please provide a valid reproducer @carverhong

  4. carverhong commented on Aug 10, 2024

    @carverhong
    Author

    Please provide a valid reproducer @carverhong

    The reproducer has been updated.

  5. added
    Issue: Author Provided ReproThis issue can be reproduced in Snack or an attached project.
    and removed
    Needs: AttentionIssues where the author has responded to feedback.
    on Aug 12, 2024
  6. shubhamguptadream11 commented on Aug 14, 2024

    @shubhamguptadream11
    Contributor

    @carverhong Is this happening on New architecture as well?

  7. 2 remaining items

  8. carverhong commented on Aug 15, 2024

    @carverhong
    Author

    Not reproducible on main Screenshot 2024-08-14 at 11 29 46 PM

    what's your XCode and iOS version ?

  9. s77rt commented on Aug 15, 2024

    @s77rt
    Contributor

    @carverhong Xcode 15.4, iOS 17.5

  10. carverhong commented on Aug 17, 2024

    @carverhong
    Author

    @s77rt @shubhamguptadream11
    react-native@0.75.1 has the same problem.

    Reproducer

    https://github.com/carverhong/react-native-reproducer-ios-image-upload/tree/rn_0_75

    Version Info

    • React Native 0.75.1
    • XCode 15.4
    • iOS 17.5

    Screenshots

    企业微信截图_3678db10-e30b-4c65-99a1-402e06a39cbe
  11. carverhong commented on Aug 17, 2024

    @carverhong
    Author

    @s77rt From your screenshot of the log, it seems that you did not use the reproducer I provided.

  12. s77rt commented on Aug 17, 2024

    @s77rt
    Contributor

    @carverhong I tested on RNTester and had problem loading the bundled file thus I used another file which worked. Turns out this is limited to bundled files.

    Screen.Recording.2024-08-17.at.6.49.43.AM-1.mov
  13. carverhong commented on Aug 17, 2024

    @carverhong
    Author

    @s77rt It doesn't matter whether to bundled the file or not. The images I provide in the reproducer are compressed. In some scenarios, we need to compress the images when we upload them to the server, because the server limits the file size. However, the image processed by RN cannot maintain the original size, resulting in unexpected upload failure.

  14. carverhong commented on Aug 17, 2024

    @carverhong
    Author

    I debugged the upload process. When RCTNetworking uploaded image, RCTImageLoader would be used to obtain the image data, and the image data would become larger when RCTImageLoader loaded the image data.

    企业微信截图_ba98ca47-894e-481e-87ce-c8777a71305e

    I wrote a demo to verify that the size of the image data does grow.

    RCT_EXPORT_METHOD(testRequestLocalFile) {
      NSString* path = [[NSBundle mainBundle] pathForResource:@"20240808144728-0" ofType:@"jpg"];
      NSString* fileUri = [NSString stringWithFormat:@"file://%@", path];
      NSURL* url = [NSURL URLWithString:fileUri];
      
      // get data from file
      NSData* rawData = [NSData dataWithContentsOfURL:url];
      NSLog(@"raw data size %lu", rawData.length);
      
      // get data from request
      NSURLRequest* request = [NSURLRequest requestWithURL:url];
      NSURLSession* session = [NSURLSession sharedSession];
      NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error != nil) {
          NSLog(@"request fail %@", error);
        } else {
          NSLog(@"data size after request %lu", data.length);
          UIImage* image = [UIImage imageWithData:data];
          NSData* data2 = UIImageJPEGRepresentation(image, 1.0);
          NSLog(@"data size after UIImageJPEGRepresentation %lu", data2.length);
        }
      }];
      [task resume];
    }
    
    企业微信截图_fc6397e0-dd89-47dd-b618-f697245fe4b0
  15. carverhong commented on Aug 17, 2024

    @carverhong
    Author

    So when RN is dealing with FormData, why doesn't it just get the file data instead of getting it by sending a request? Getting the data directly from the file does not present this problem.

  16. s77rt commented on Aug 17, 2024

    @s77rt
    Contributor

    The problem seems due to two causes:

    1. RCTImageLoader is checked first before RCTFileRequestHandler, for bundled files the check passes and we use RCTBundleAssetImageLoader where for other data files this will fall-through and uses the RCTFileRequestHandler as expected.

    Flowchart

    1. RCTFileRequestHandler does not support bundled assets, this was done to fix image flickering 5903949#diff-2c4b872c9bed6284eb88ee0c20cab44a8e6ab1c0d3c1789e2edbde0e7637f4de cc @janicduplessis

    https://github.com/facebook/react-native/blob/d9f684b1cf7fb6824e25e1e948bfb30dec450464/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm#L32-L35

  17. carverhong commented on Aug 18, 2024

    @carverhong
    Author

    I'm not sure if there are other considerations for RCTNetworking to handle FormData in this way. This problem has already affected our production users. If we want to fix this problem, is it reasonable to directly read local image file data into FormData instead of relying on RCTImageLoader?

  18. ziggear commented on Aug 20, 2024

    @ziggear

    Hi @carverhong @s77rt @shubhamguptadream11 , I find this issue quite interesting. Please allow me to explain the problem as I understand it.

    I believe this issue is not primarily related to RCTBundleAssetImageLoader but rather to the design intent of RCTImageLoader class. From what I can see, RCTImageLoader serves two main purposes:

    1. Image Display: When you create an <Image /> component, on the iOS side, RCTImageView interacts with RCTImageLoader, which then loads the corresponding UIImage object for RCTImageView. Before this pull was merged, meaning in version 0.72 and earlier, this task was primarily handled by RCTLocalAssetImageLoader. Since developers could not easily read data from the Bundle or Assets, they couldn’t dynamically retrieve a complete URI. With RCTLocalAssetImageLoader, the <Image /> component could display images from Assets or the Bundle without requiring a full URI, using calls like this:
    <Image source={{ uri: 'logo' }} style={{ width: 100, height: 100 }} />
    1. Image Upload: This is the part that confuses me. When you obtain a real URI by some method—for example, in @carverhong ’s reproducer, a custom NativeModule was implemented to achieve this—RCTImageLoader is called when developers use fetch or axios to upload images. The - (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate method in RCTImageLoader is invoked, where UIImagePNGRepresentation/UIImageJPEGRepresentation is used to convert the already loaded UIImage into NSData, which is then assembled into the NSURLResponse for the final actual request. However, UIImagePNGRepresentation/UIImageJPEGRepresentation alters the file corresponding to that URI. Due to differences in the png compression algorithm, images in different png formats exhibit varying changes in size.

    Here’s the code I used to reproduce the issue (same PhotoModule from here):

    NativeModules.PhotoModule.getPhoto().then(photo => {
      console.log(photo.uri);
    
      const formData = new FormData();
      formData.append('file', {
        uri: photo.uri,
        type: photo.mime,
        name: photo.name,
      });
    
      fetch('http://localhost:3000/upload', {
        method: 'POST',
        body: formData,
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      })
      .then((response) => response.json())
      .then((result) => {
        // pass
      });
    });

    I believe this behavior is not expected because some file upload APIs might require developers to sign the file, and the modifications made by UIImagePNGRepresentation/UIImageJPEGRepresentation can cause the signature to fail. I tried to understand the original intent behind this code. Perhaps the idea was that since the UIImage is already available, converting it to NSData would save an additional file I/O operation? Or maybe in some scenarios that I’m unaware of, caching the UIImage in memory could be beneficial for performance? Since my understanding of this part of the code is limited, I hope someone can clarify this issue.

    Finally, I made a simple modification that I believe could resolve the issue, though it hasn't been thoroughly tested. @carverhong , you might want to check if this meets your requirements. The change involves reading the image data directly from request.URL in the sendRequest:withDelegate: method of RCTImageLoader:

    if (RCTImageHasAlpha(image.CGImage)) {
      mimeType = @"image/png";
      imageData = [NSData dataWithContentsOfURL:request.URL];
    } else {
      mimeType = @"image/jpeg";
      imageData = [NSData dataWithContentsOfURL:request.URL];
    }

    I hope this can be of some help 😁

  19. carverhong commented on Aug 21, 2024

    @carverhong
    Author

    @ziggear Do not use the UIImagePNGRepresentation/UIImageJPEGRepresentation can solve this problem, but modify RCTImageLoader may produce side effects to normal pictures show? I recently looked at the Android source code. The Android side provides RequestBodyUtil, which handles FormData by determining that the uri scheme contains http keywords before sending a network request to load the file data. This problem affects the upload of FormData on iOS. In my opinion, the core of the problem is to ensure the accuracy of FormData. Therefore, I prefer to modify RCTHTTPFormDataHelper in RCTNetworking rather than RCTImageLoader. I think RCTHTTPFormDataHelper should read the raw file data directly through the uri when working with local files, rather than sending a network request. But I'm not sure why RCTHTTPFormDataHelper was designed to get local file data by making a network request.

  20. ziggear commented on Aug 21, 2024

    @ziggear

    Therefore, I prefer to modify RCTHTTPFormDataHelper in RCTNetworking rather than RCTImageLoader. I think RCTHTTPFormDataHelper should read the raw file data directly through the uri when working with local files, rather than sending a network request.

    Of course. I believe that both changes can resolve this issue.

    However, I would waiting an official clarification. What are the intended responsibilities of the RCTImageLoader, and why are the priorities of the handlers in the prioritizedHandlers method of RCTNetworking arranged in current order.

    I still working on it, too, and this pull may help.

  21. carverhong commented on Sep 9, 2026

    @carverhong
    Author

    Summary

    When a multipart part references a local image via a file:// URI, RCTNetworking routes the part through RCTImageLoader instead of RCTFileRequestHandler. RCTImageLoader decodes the file into a UIImage and re-encodes it with UIImageJPEGRepresentation(image, 1.0) — or UIImagePNGRepresentation when the decoded bitmap has an alpha channel. As a result:

    • the uploaded bytes are not the bytes on disk: a JPEG deliberately compressed to a size budget (e.g. a few hundred KB) can balloon to several MB;
    • the part's declared type is discarded and replaced by image/jpeg / image/png.

    Android does not exhibit this behavior (OkHttp streams the file as-is), so identical JS upload code produces different payloads per platform.

    Causal chain

    1. JS: the app appends a part to FormData and POSTs it via XHR / fetch:

      formData.append('fileContent', {
        uri: 'file:///var/mobile/.../Library/Caches/.../photo.jpg', // sandbox path, written by the camera pipeline
        type: 'image/jpeg',
        name: 'photo.jpg',
      });
    2. FormData polyfill (Libraries/Network/FormData.js, getParts()): the value becomes a part {uri, type, name, fieldName, headers} — per RN's own doc comment, a "blob" in React Native is "an object with a uri attribute".

    3. RCTNetworking (Libraries/Network/RCTNetworking.mm, processDataForHTTPQuery:): the formData branch hands the parts to RCTHTTPFormDataHelper process:, which processes them one by one.

    4. Part → internal request: for each part carrying a uri, the helper wraps the URI into an internal NSURLRequest and dispatches it via networkTaskWithRequest: → handlerForRequest:, which selects the highest-priority registered RCTURLRequestHandler:

      • RCTImageLoader — handlerPriority = 2 (RCTImageLoader.mm:156) → wins
      • RCTFileRequestHandler — no handlerPriority (default 0). This is the handler that would simply read the raw file bytes; its canHandleRequest: (RCTFileRequestHandler.mm:32) matches any non-bundle file:// URL.
    5. Why RCTImageLoader claims it (RCTImageLoader.mm:1110, canHandleRequest:): it asks its RCTImageURLLoader chain. RCTLocalAssetImageLoader claims any URL under the app's home directory whose path extension is exactly png or jpg:

      // React/Base/RCTUtils.m
      static BOOL RCTIsImageAssetsPath(NSString *path) {   // :731
        NSString *extension = [path pathExtension];
        return [extension isEqualToString:@"png"] || [extension isEqualToString:@"jpg"];
      }
      BOOL RCTIsLocalAssetURL(NSURL *imageURL) {           // :752
        return RCTIsBundleAssetURL(imageURL) || RCTIsHomeAssetURL(imageURL);
      }

      A camera photo written to the app sandbox with a .jpg extension therefore matches.

    6. The re-encode (RCTImageLoader.mm:1148, sendRequest:withDelegate:): loadImageWithURLRequest: decodes the file into a UIImage, then re-encodes it:

      if (RCTImageHasAlpha(image.CGImage)) {
        mimeType = @"image/png";
        imageData = UIImagePNGRepresentation(image);
      } else {
        mimeType = @"image/jpeg";
        imageData = UIImageJPEGRepresentation(image, 1.0);
      }

      The decoded-then-re-encoded bytes become the part body, and the reported MIME type overrides the part's declared type.

    7. The re-encoded body is assembled into the multipart payload and sent. The server receives different bytes (and potentially a different part content-type) than the file on disk.

    Notably, RCTImageLoader.mm:1137 already acknowledges this anti-pattern:

    Don't use RCTImageURLLoader protocol for modules that already conform to RCTURLRequestHandler as it's inefficient to decode an image and then convert it back into data

    …but RCTLocalAssetImageLoader does not conform to RCTURLRequestHandler, so local file:// images fall squarely into the inefficient path the comment warns about.

  22. carverhong commented on Sep 9, 2026

    @carverhong
    Author

    Workaround without patching the framework: RCTNetworkingRequestHandler

    RCTNetworking exposes an official request-body extension point, RCTNetworkingRequestHandler (RCTNetworking.h). processDataForHTTPQuery: polls registered request handlers before any built-in branch (string / base64 / uri / formData); a handler returning {body, contentType} short-circuits the entire default chain, and buildRequest assigns the result directly to HTTPBody. RCTBlobManager is the in-tree reference for this pattern (registration in RCTBlobManager.mm addNetworkingHandler, implementation via canHandleNetworkingRequest / handleNetworkingRequest).

    An app-level handler that only takes over when no part references a remote/non-file URI — falling back to the default chain otherwise — avoids the decode/re-encode roundtrip entirely:

    - (BOOL)canHandleNetworkingRequest:(NSDictionary *)data
    {
      NSArray<NSDictionary *> *formData = data[@"formData"];
      if (formData.count == 0) {
        return NO;
      }
      for (NSDictionary *part in formData) {
        NSString *uri = part[@"uri"];
        // String parts (no uri) are fine to handle here.
        // Any remote or non-file URI → decline and let the default chain take over.
        if (uri && ![uri hasPrefix:@"file://"]) {
          return NO;
        }
      }
      return YES;
    }
    
    - (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data
    {
      // Build the multipart body ourselves:
      //  - generate a random boundary (same approach as RCTGenerateFormBoundary in RCTNetworking.mm)
      //  - file parts   → [NSData dataWithContentsOfURL:] raw bytes, content-type from the part's declared type
      //  - string parts → UTF-8 encoded
      return @{
        @"body": multipartBody,
        @"contentType": [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary],
      };
    }

    Registration follows the RCTBlobManager pattern: fetch the Networking module from the module registry and call addRequestHandler: on its method queue.

    This keeps the upload logic in app code, requires no node_modules patching, and survives RN upgrades without rebasing a patch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Component: ImageIssue: Author Provided ReproThis issue can be reproduced in Snack or an attached project.Needs: ReproThis issue could be improved with a clear list of steps to reproduce the issue.Platform: iOSiOS applications.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions