Repository navigation
[iOS] Uploading an image will change the file size of the image #45962
Copy link
Copy link
Open
Labels
Component: ImageIssue: Author Provided ReproThis issue can be reproduced in Snack or an attached project.This 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.This issue could be improved with a clear list of steps to reproduce the issue.Platform: iOSiOS applications.iOS applications.
Description
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
-
Clone the reproducer from
https://github.com/carverhong/react-native-reproducer-ios-image-upload.git. -
Install dependency and prepare the ios project.
cd react-native-reproducer-ios-image-upload
npm install
bundle install
cd ios
bundle exec pod installAfter the installation is complete, the RCTNetworking log patch is automatically added for debugging.
- Start Metro.
npm start- Open
HelloWorld.xcworkspacewith XCode and run the APP. - Use the
RCTNetworkingkeyword to filter logs.
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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Component: ImageIssue: Author Provided ReproThis issue can be reproduced in Snack or an attached project.This 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.This issue could be improved with a clear list of steps to reproduce the issue.Platform: iOSiOS applications.iOS applications.
Activity
react-native-bot commentedon Aug 9, 2024
react-native-bot commentedon Aug 9, 2024
cortinico commentedon Aug 9, 2024
Please provide a valid reproducer @carverhong
carverhong commentedon Aug 10, 2024
The reproducer has been updated.
shubhamguptadream11 commentedon Aug 14, 2024
@carverhong Is this happening on New architecture as well?
2 remaining items
carverhong commentedon Aug 15, 2024
what's your XCode and iOS version ?
s77rt commentedon Aug 15, 2024
@carverhong Xcode 15.4, iOS 17.5
carverhong commentedon Aug 17, 2024
@s77rt @shubhamguptadream11
react-native@0.75.1has the same problem.Reproducer
https://github.com/carverhong/react-native-reproducer-ios-image-upload/tree/rn_0_75
Version Info
Screenshots
carverhong commentedon Aug 17, 2024
@s77rt From your screenshot of the log, it seems that you did not use the reproducer I provided.
s77rt commentedon Aug 17, 2024
@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
carverhong commentedon Aug 17, 2024
@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.
carverhong commentedon Aug 17, 2024
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.
I wrote a demo to verify that the size of the image data does grow.
carverhong commentedon Aug 17, 2024
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.
s77rt commentedon Aug 17, 2024
The problem seems due to two causes:
RCTImageLoaderis checked first beforeRCTFileRequestHandler, for bundled files the check passes and we useRCTBundleAssetImageLoaderwhere for other data files this will fall-through and uses theRCTFileRequestHandleras expected.RCTFileRequestHandlerdoes not support bundled assets, this was done to fix image flickering 5903949#diff-2c4b872c9bed6284eb88ee0c20cab44a8e6ab1c0d3c1789e2edbde0e7637f4de cc @janicduplessishttps://github.com/facebook/react-native/blob/d9f684b1cf7fb6824e25e1e948bfb30dec450464/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm#L32-L35
carverhong commentedon Aug 18, 2024
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?
ziggear commentedon Aug 20, 2024
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
RCTBundleAssetImageLoaderbut rather to the design intent ofRCTImageLoaderclass. From what I can see,RCTImageLoaderserves two main purposes:<Image />component, on the iOS side,RCTImageViewinteracts withRCTImageLoader, which then loads the correspondingUIImageobject forRCTImageView. Before this pull was merged, meaning in version 0.72 and earlier, this task was primarily handled byRCTLocalAssetImageLoader. Since developers could not easily read data from the Bundle or Assets, they couldn’t dynamically retrieve a complete URI. WithRCTLocalAssetImageLoader, the<Image />component could display images from Assets or the Bundle without requiring a full URI, using calls like this:RCTImageLoaderis called when developers use fetch or axios to upload images. The- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegatemethod inRCTImageLoaderis invoked, whereUIImagePNGRepresentation/UIImageJPEGRepresentationis used to convert the already loaded UIImage intoNSData, which is then assembled into theNSURLResponsefor the final actual request. However,UIImagePNGRepresentation/UIImageJPEGRepresentationalters 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):
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/UIImageJPEGRepresentationcan 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.URLin thesendRequest:withDelegate:method ofRCTImageLoader:I hope this can be of some help 😁
carverhong commentedon Aug 21, 2024
@ziggear Do not use the
UIImagePNGRepresentation/UIImageJPEGRepresentationcan 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 providesRequestBodyUtil, which handles FormData by determining that the uri scheme containshttpkeywords 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 modifyRCTHTTPFormDataHelperin 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.ziggear commentedon Aug 21, 2024
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 theprioritizedHandlersmethod ofRCTNetworkingarranged in current order.I still working on it, too, and this pull may help.
carverhong commentedon Sep 9, 2026
Summary
When a multipart part references a local image via a
file://URI,RCTNetworkingroutes the part throughRCTImageLoaderinstead ofRCTFileRequestHandler.RCTImageLoaderdecodes the file into aUIImageand re-encodes it withUIImageJPEGRepresentation(image, 1.0)— orUIImagePNGRepresentationwhen the decoded bitmap has an alpha channel. As a result:typeis discarded and replaced byimage/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
JS: the app appends a part to
FormDataand POSTs it via XHR / fetch: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 auriattribute".RCTNetworking (
Libraries/Network/RCTNetworking.mm,processDataForHTTPQuery:): theformDatabranch hands the parts toRCTHTTPFormDataHelper process:, which processes them one by one.Part → internal request: for each part carrying a
uri, the helper wraps the URI into an internalNSURLRequestand dispatches it vianetworkTaskWithRequest:→handlerForRequest:, which selects the highest-priority registeredRCTURLRequestHandler:RCTImageLoader—handlerPriority= 2 (RCTImageLoader.mm:156) → winsRCTFileRequestHandler— nohandlerPriority(default 0). This is the handler that would simply read the raw file bytes; itscanHandleRequest:(RCTFileRequestHandler.mm:32) matches any non-bundlefile://URL.Why
RCTImageLoaderclaims it (RCTImageLoader.mm:1110,canHandleRequest:): it asks itsRCTImageURLLoaderchain.RCTLocalAssetImageLoaderclaims any URL under the app's home directory whose path extension is exactlypngorjpg:A camera photo written to the app sandbox with a
.jpgextension therefore matches.The re-encode (
RCTImageLoader.mm:1148,sendRequest:withDelegate:):loadImageWithURLRequest:decodes the file into aUIImage, then re-encodes it:The decoded-then-re-encoded bytes become the part body, and the reported MIME type overrides the part's declared
type.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:1137already acknowledges this anti-pattern:…but
RCTLocalAssetImageLoaderdoes not conform toRCTURLRequestHandler, so localfile://images fall squarely into the inefficient path the comment warns about.carverhong commentedon Sep 9, 2026
Workaround without patching the framework:
RCTNetworkingRequestHandlerRCTNetworkingexposes 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, andbuildRequestassigns the result directly toHTTPBody.RCTBlobManageris the in-tree reference for this pattern (registration inRCTBlobManager.mmaddNetworkingHandler, implementation viacanHandleNetworkingRequest/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:
Registration follows the
RCTBlobManagerpattern: fetch theNetworkingmodule from the module registry and calladdRequestHandler:on its method queue.This keeps the upload logic in app code, requires no
node_modulespatching, and survives RN upgrades without rebasing a patch.