1.0 rewrites the native layers and cleans up the JavaScript API. Android moves from Java to Kotlin, and iOS from Objective-C++ to Swift; Windows stays C++. Before the old Android and iOS code was removed, the new code was checked call by call against recordings of how 0.25 behaved on the same device. On top of that, the JavaScript surface loses what never worked, and the platforms are brought to one behaviour where they disagreed. Every change to what your code sees is listed under JavaScript API.
Most apps only need to meet the new requirements below and check the removed APIs.
| 0.25 | 1.0 | |
|---|---|---|
| React Native architecture | Old or New | New Architecture only |
| React Native | 0.76 and up | 0.84 and up (tested on 0.84 and the newest release); peerDependencies now says so, so npm install on an older app fails instead of the build |
Android minSdk |
the app's (library fallback 16) | 24 |
| Android build | Java 8 bytecode | Java 17 bytecode, Kotlin compiled with the app's Kotlin setup |
| iOS deployment target | 11.0 | 15.1 |
| Xcode | 16.1 or newer | |
| iOS integration | CocoaPods or the bundled Xcode project | CocoaPods, or SwiftPM autolinking on React Native 0.87 and newer (1.1.0) |
If your app still runs on the Old Architecture, stay on 0.25 until it moves to the New Architecture.
The API is grouped by what a call does, not by which platform implements it. A call a
platform cannot make rejects with ENOTSUP. The old names keep working for one release
and print one deprecation warning each.
| Before | 1.0 |
|---|---|
android.actionViewIntent(path, mime) / ios.presentPreview(path) |
open.file(path, {mime, scheme}) |
android.actionViewIntent(path, mime, title) / ios.presentOpenInMenu(path) |
open.chooser(path, {mime, scheme, title}) |
ios.presentOptionsMenu(path) |
open.optionsMenu(path, {scheme}) |
android.getContentIntent(mime) |
open.pick(mime) (resolves null when cancelled) |
MediaCollection.createMediaFile |
media.createFile(fd, collection) |
MediaCollection.writeToMediaFile / ...WithTransform |
media.write(uri, path, {transform}) (resolves undefined; the old names keep resolving "Success") |
MediaCollection.copyToMediaStore, copyToInternal |
media.copyToMediaStore, media.copyToInternal |
MediaCollection.getBlob(uri, encoding) |
media.read(uri, encoding) |
android.addCompleteDownload(options) |
media.addDownload(options) |
android.scanFile(files), fs.scanFile |
media.scan(files) |
android.getSDCardDir(), getSDCardApplicationDir() |
fs.sdCardDir(), fs.sdCardApplicationDir() |
fs.lstat(path) |
fs.ls(path, {stats: true}) (lstat means "stat without following a link" in Node) |
ios.excludeFromBackupKey(path) |
fs.excludeFromBackup(path) |
ios.pathForAppGroup, syncPathAppGroup, fs.pathForAppGroup, fs.syncPathAppGroup |
fs.appGroupDir(name), fs.appGroupDirSync(name) |
fs.readFileWithTransform(path, encoding) |
fs.readFile(path, encoding, {transform: true}) |
fs.writeFileWithTransform(path, data, encoding) |
fs.writeFile(path, data, encoding, {transform: true}) |
- The Web API polyfills (
ReactNativeBlobUtil.polyfill: Blob, File, XMLHttpRequest, FileReader, Fetch, ProgressEvent, Event). They were experimental since 0.8 and no longer worked: FileReader's read methods were stubs, and XMLHttpRequest never completedjsonorarraybufferresponses. Use React Native's ownBlob,XMLHttpRequestandfetch; for file bodies and file downloads, useReactNativeBlobUtil.fetchwithwrap(path)andconfig({path}). ReactNativeBlobUtil.JSONStreamand the bundled oboe.js. It replaced the global XMLHttpRequest when used. Stream the response to a file withconfig({path})and parse it, or use a streaming JSON parser of your choice onfs.readStream.response.blob()on the result offetch. It returned the polyfill Blob. Useresponse.base64(),response.path()(withconfig({fileCache: true})orconfig({path})) or the newresponse.array().fetchof aReactNativeBlobUtil-file://URL. It read the file through the stream API withoutcancel,taskIdor progress. Usefs.readFileorfs.readStream.
The same call resolved different values per platform. 1.0 resolves one value everywhere, by
one rule: a call resolves undefined unless it returns something the caller does not
already have (a byte count, a stat, a new content URI).
| Call | 0.25 | 1.0 |
|---|---|---|
fs.createFile |
the path (Android), [null] (iOS), undefined (Windows) |
undefined |
fs.cp, fs.mv |
undefined (Android), true (iOS, Windows) |
undefined |
fs.mkdir |
true or undefined by platform |
undefined |
fs.slice |
the destination path | undefined |
media.copyToInternal |
"" (Android) |
undefined; the deprecated MediaCollection.copyToInternal keeps "" |
fs.df |
four strings internal_free, internal_total, external_free, external_total (Android); {free, total} numbers (iOS, Windows) |
{free, total} as numbers on every platform; Android keeps its four fields, as numbers |
fs.lstat entries |
size a string; lastModified a string (Android) or number (iOS) |
size and lastModified numbers, like fs.stat |
fs.readFile(path, 'ascii'), ascii readStream chunks, response.array() |
bytes -128..127 (Android, iOS) or 0..255 (Windows) | bytes 0..255 |
ios.presentOptionsMenu, ios.presentOpenInMenu, ios.presentPreview, ios.excludeFromBackupKey |
[null] (iOS), [] (Windows) |
undefined |
If you compared ascii bytes against negative values, or read df().internal_free as a
string, adjust those comparisons.
- A call that only exists on one platform now rejects on the others with an
ErrorwhosecodeisENOTSUP. Before,android.*andios.present*rejected with a bare string,fs.pathForAppGroupandios.excludeFromBackupKeynever settled on Android, and MediaCollection resolved""or[]on Windows as if it had worked. fs.scanFileis nowmedia.scan;fs.pathForAppGroupandfs.syncPathAppGroupare nowfs.appGroupDirandfs.appGroupDirSync. The old names still work and print one deprecation warning.ios.openDocumentandios.previewDocumentwere crossed:openDocumentshowed the preview andpreviewDocumentthe options menu. They now do what their names say (the options menu and the preview), and are deprecated in favour ofopen.optionsMenuandopen.file.MediaCollection.createMediaFile,writeToMediaFileandwriteToMediaFileWithTransformare the spellings that match native; the old...Mediafilenames still work and warn once.- A cancelled
fetchrejects withCanceledFetchError, which now hascode: 'ECANCELED'and is also a named export:import {CanceledFetchError} from 'react-native-blob-util'.
- Named exports:
import {fetch, config, fs, open, media, wrap, session, base64} from 'react-native-blob-util'. They are the same objects as the default export's members. CodedError.codeis the exportedErrorCodeunion, andFetchErroraddsrespInfofor a failedfetch.readFile,writeFile,appendFileandmedia.readaccept an encoding typed asEncoding(known only at runtime) and resolvestring | number[].android.actionViewIntentis typed to resolvevoid, as it does at runtime.index.js.flowis gone;index.d.tsis the only declaration file and now matches the JavaScript. Names that changed:RNFetchBlobDfisReactNativeBlobUtilDf(the old name stays as a deprecated alias),ReactNativeBlobUtilStreamisReactNativeBlobUtilReadStream(alias kept),ReactNativeBlobUtilFileandNetare removed (nothing implemented them), andconfig()returns{fetch}rather than the whole API.StatefulPromisegainsstateChange,partandtaskId;readFile,createFileandwriteFileare typed per encoding.
Every rejection now carries a code. Before, the native methods that reported through a
callback (exists, writeStream, the write stream's write and close, unlink,
session.dispose, stat, lstat, cp, mv, df, scanFile, cancelRequest)
either stamped EUNSPECIFIED on every failure or gave no code at all, and a fetch
error had neither a code nor the response info. Now:
-
stat,lstat,cpandmvreject withENOENTwhen the source, or the destination's directory, is missing.writeStreamrejects withEISDIRfor a directory,ENOENTwhen the file cannot be created andENOTDIRwhen its parent cannot. Writing to or closing a stream that was already closed rejects withEBADF(on Android this used to crash the app). Anything else keepsEUNSPECIFIED. -
A failed
fetchrejects with anErrorwhoserespInfoholds the response info received so far and whosecodenames the failure the same way on every platform:Code Meaning ETIMEDOUTthe request timed out ( respInfo.timeoutis also true)ENOTFOUNDthe host name could not be resolved ECONNREFUSEDthe host refused the connection ECONNRESETthe connection was lost ENETUNREACHno usable network (Android wifiOnlywithout WiFi; iOS offline)ESSLthe TLS handshake or certificate check failed, including customCACertsandpinnedHostsrejectionsECANCELEDthe task was cancelled EINVALthe URL or method is invalid ENOTDIRthe download directory could not be created EUNSPECIFIEDanything else; the message says what -
EACCES(new): acontent://provider refused access to the URI (Android). -
task.cancel(callback): the callback is called once native has cancelled, with an error argument if that failed. It used to receive(null, taskId). -
Messages are unchanged where they existed, so string matching on messages keeps working; switch to
err.codewhen you can.
Where the platforms disagreed, 1.0 picks one behaviour:
| Operation | Before | 1.0 |
|---|---|---|
fs.unlink on a missing path |
rejected on Android, resolved on iOS | resolves everywhere |
session.dispose with a missing file |
resolved on Android, rejected on iOS | resolves everywhere |
fs.cp / fs.mv onto an existing file |
Android overwrote, iOS and Windows rejected (cp) | overwrite everywhere |
fs.writeFile onto a directory |
ENOENT (Android), EISDIR (iOS) | EISDIR |
fs.writeStream onto a directory |
EUNSPECIFIED (Android), EISDIR (iOS) | EISDIR |
fs.writeFile with invalid base64 |
resolved 0 (Android), EINVAL (iOS) | EINVAL |
fs.readFile on a directory |
EISDIR (Android), resolved "" (iOS) | EISDIR |
fs.readFile utf8 of invalid bytes |
U+FFFD (Android), null (iOS) | U+FFFD |
utf8 readStream with a character split across chunks |
ok (Android), error (iOS) | ok |
fs.createFile(path, src, 'uri') with a missing source |
ENOENT (Android), empty file (iOS) | ENOENT |
fs.hash / fs.slice on a missing file |
EUNSPECIFIED (Android), ENOENT (iOS) | ENOENT |
fs.mkdir on an existing directory |
EEXIST (Android, iOS), resolved (Windows) | EEXIST |
fs.ls on a missing path |
ENOENT (Android, iOS), ENOTDIR (Windows) | ENOENT |
lstat lastModified on Windows |
seconds | milliseconds, like the other platforms |
android.getContentIntent when the user cancels |
never settled | resolves null; a second call while the picker is open rejects EBUSY |
android.actionViewIntent |
resolved true, then null again on resume |
settles once; resolves undefined, like open.file it forwards to |
respInfo.respType on Android |
"" for every text response (a broken header check) |
text, json or blob by Content-Type, as on iOS |
config({auto: true}) on Android |
wrote only a type listed in binaryContentTypes to a file (the same broken check) |
writes every response that is not text/* or application/json to a file, as on iOS (1.1.0) |
An upload on Android whose source cannot be read (a revoked content:// URI, a missing asset, a read error mid-way) |
sent an empty or cut-off body, and either resolved or failed with "unexpected end of stream" | rejects with the source's own error (EUNSPECIFIED) |
appendExt containing /, \, : or a control character (GHSA-5xf6-f6v8-jc8c) |
appended to the generated cache file name as it was, so "/../../shared_prefs/x.xml" wrote the download over any file the app can write |
rejects EINVAL on every platform; an extension such as png, tar.gz or .jpg is unaffected |
| A header name or value containing CR, LF or NUL | sent as it was: rejected by OkHttp on Android, and on Windows it started a new header on the wire | rejects EINVAL before the request starts, on every platform |
A multipart name or filename containing ", CR or LF |
pasted into the part header unescaped (Android, iOS), so it could rewrite the part's headers | escaped as browsers do: %22, %0D, %0A; a type with CR or LF rejects EINVAL |
pinnedHosts written with capitals |
never matched (hosts are compared as the HTTP stack reports them, in lower case), so the request used system trust | matched case-insensitively |
res.text() / res.json() of a fileCache/path or base64 response |
decoded one character per byte, so é came back as é; res.base64() of a text body threw on characters beyond Latin-1 |
UTF-8 on every platform, a leading byte order mark dropped (as fetch does), an embedded NUL kept on iOS too |
Content-Length of a single-file upload over 2 GB on Android |
capped at 2 GB, so the request declared the wrong length | the file's size |
fs.slice on Windows |
wrote the slice into the source file, from an empty buffer | writes the range to the destination |
fs.ls on Windows |
listed the parent directory | lists the directory itself |
session.dispose on Windows |
failed on every file | removes the files; missing ones are skipped |
fs.dirs on Windows |
7 directories set | every key set (Android-only ones are "") |
| Events on iOS | a JSON string each listener had to parse | the object itself, as on Android |
ios.excludeFromBackupKey of a path with # or a space |
marked a different file, or resolved without marking one | marks the file it was given; EINVAL when no URL can be built |
One difference cannot be aligned from this side. A string a native module
resolves reaches JS through jsi::String::createFromUtf8(runtime, [value UTF8String])
(RCTTurboModule.mm), which reads a C string, so on iOS it stops at the first NUL
byte: fs.readFile(path, 'utf8') of content holding a NUL returns everything up to
it, where Android returns the whole thing. The native layer produces the full string -
what is cut off is cut off above it. base64 and ascii are unaffected, and are how
to read a file that is not text.
Network-level differences stay documented rather than aligned: a URL without a host
(http://) is EINVAL on Android, which rejects it before connecting, and
ECONNREFUSED on iOS, which tries to connect; and only iOS puts rnfbEncode on the first
stateChange.
The result of fetch gains what a fetch Response offers first:
res.status,res.ok(status 200..299),res.headers(names in lower case) andres.url(the URL after redirects), next tores.info(), which is unchanged. An HTTP error status still resolves; withconfig({path})the error body is what was written to the file, so checkres.okbefore using it.res.arrayBuffer().res.readFile()andres.readStream()reject withEINVAL, andres.session()throws, when the body is not a file. They used to log a warning and returnnull.
0.25 let each platform guess what a string body was, from its Content-Type and a
prefix, and they guessed differently: without a Content-Type, iOS sent the string as
application/octet-stream and base64-decoded it while Android sent it as text, and
Windows never decoded base64 and did not recognise wrap(path) at all. 1.0 decides
in JavaScript and every platform sends what it is told.
- A plain string keeps the rule 0.x documented: a
wrap(path)string is a file; a Content-Type ending in;base64(removed before sending) or starting withapplication/octetmakes it base64; anything else is text. Without a Content-Type a string is text on every platform (iOS decoded it as base64). - A body sent without a Content-Type gets one, as fetch does for a string: text goes
out as
text/plain;charset=UTF-8, base64, bytes and files asapplication/octet-stream. The platforms disagreed: Android sent none, Windowstext/plain; charset=UTF-8, and iOS's URLSession addsapplication/x-www-form-urlencodedby itself. - New explicit forms, never guessed from anything:
{text: string},{base64: string},{file: path}(a path, or acontent://URI on Android), and bytes (ArrayBufferor a typed array). Use{text}for a string that comes from a user or a server: a plain string that happens to start with the file prefix is uploaded as that file's contents. - A multipart field's
datamay use the same forms. A plain string is text without afilenameand base64 (or a wrapped file) with one, as before; a file part without afilenameis named after the file, as browsers do. - A body on
GETorHEADrejectsEINVAL(it was dropped silently).DELETEandOPTIONSsend the body they are given (Android dropped it). - A file body whose file does not exist rejects
ENOENT. Android used to create an empty file at that path and upload it.
response.text(), response.json() and response.base64() returned a value when the
body was held in memory and a Promise when it was a file, so code worked or broke depending
on config(). They always return a Promise now; await them. response.flush() always
returns a Promise (resolved at once when there is no file). task.cancel() returns a
Promise that resolves once native has cancelled; the optional callback still works.
-
The config keys
Progress,UploadProgressandindicatorare gone. Nothing read the first two (usetask.progress({interval, count}, fn)), and the iOS network activity indicator has not existed since iOS 13. -
task.expire(fn)is gone. No platform ever emitted the event it listened for. -
keyis now declared in the config types; it was always honoured. So areautoandbinaryContentTypes(1.1.0), honoured on Android and iOS, and theRNFB-Responserequest header, which forces the form a body held in memory takes. -
media.addDownloadtakesnotification(1.1.0), the key the DownloadManager config uses for the same thing;showNotificationstill works and warns once. -
URIUtil.isFileURIandURIUtil.unwrapFileURIrecognise theReactNativeBlobUtil-content://prefixwrap()gives a content URI (1.1.0); they only knew the file prefix. -
fs.readStreamreads 12288 bytes per chunk by default (a multiple of 3, so base64 chunks concatenate) instead of 10240, andtickdefaults to 10 ms in both the wrapper and the stream. -
Every call that took positional flags also takes one options object; the positional forms keep working:
fs.readFile(path, {encoding, transform}),fs.writeFile(path, data, {encoding, transform}),fs.appendFile(path, data, {encoding}),fs.createFile(path, data, {encoding}),fs.readStream(path, {encoding, bufferSize, tick}),fs.writeStream(path, {encoding, append}),open.pick({mime}),media.read(uri, {encoding}). -
mimeis the key for a MIME type everywhere. Media descriptors take{name, mime, parentFolder};mimeTypestill works. -
configgroups the platform-only options and renames the transformer flag:Before 1.0 transformFiletransform(as inreadFile/writeFile)addAndroidDownloadsandroid: {downloadManager}wifiOnly,targetHostIpandroid: {wifiOnly, targetHostIp}IOSBackgroundTaskios: {backgroundTask}The old keys keep working and warn once each.
-
media.readrejects an unknown encoding withEINVAL, asfs.readFiledoes; it was read as utf8. -
A read stream's
open()returns a Promise that resolves when the stream ends and rejects with the error when it fails. An error withoutonErrorused to be dropped; now it rejects that Promise (withonErrorset, the rejection is marked handled). The native listener is added byopen(), so a stream that is never opened no longer leaves it behind, and opening a finished stream rejectsEBADFinstead of throwing.fs.readStream('')rejectsEINVALinstead of throwing. -
task.progress({interval: 0}, fn)reports every chunk;0used to become250. -
open.*on iOS accepts a path that is already afile://URL; it was prefixed twice.
Apps that follow the README don't need to change anything.
0.25 turned a content:// URI into a file path before using it: the provider's
_data column, the path inside a Downloads raw: id, or an external-storage document
id joined onto a directory. That path was then opened with your app's own rights, so a
URI another app shared with yours could name any file your app can read or delete,
including its private files. fs.readFile, readStream, stat and unlink on a
forged Downloads URI read and deleted a file in your app's data directory.
1.0 hands every content:// URI to ContentResolver, and the provider decides:
readFile,readStream,hash,cp(source and destination),writeStream,media.write,media.copyToMediaStoreand uploads ofwrap(uri)read or write through the provider. A URI the provider refuses rejects withEACCES.statasks the provider:filename,sizeandlastModifiedas it reports them,type: 'file', andpathis the URI itself. It no longer returns a file path for a content URI; pass the URI to the other calls instead.existsasks the provider (isDirectoryis false);unlinkdeletes through it, and resolves when there was nothing to delete.ls,lstat,mv,mkdir,createFile,writeFileand the destination ofsliceonly take file paths and reject a content URI withENOTSUP. Copy it to a file withfs.cp(uri, path)first.com.ReactNativeBlobUtil.Utils.PathResolveris gone.
-
trusty: truekeeps hostname verification. It used to install an accept-all hostname verifier next to yoursharedTrustManager, so a trust manager that validated a private CA still accepted that CA's certificate for any host name. If your test server's certificate does not name the host you connect to, fix the certificate (or usecustomCACerts, the supported way to trust a private CA). -
customCACertswithpinnedHostsis decided per TLS handshake, so a redirect is covered: a request that started at an unpinned host and was redirected to a pinned one reached the pinned host with the system's trust.customCACertsApplyTois gone fromReactNativeBlobUtilUtils;getCustomCACertOkHttpClienttakes the pinned hosts. -
A DownloadManager download no longer leaves its broadcast receiver registered for the life of the app, and an unrelated
DOWNLOAD_COMPLETEbroadcast without an id no longer crashes it. -
Before Android 10,
media.createFile/copyToMediaStorereject anameorparentFoldercontaining a..segment; the file used to be created wherever the joined path pointed. -
Request URLs are no longer written to logcat when a request fails to build.
-
media.*and the SD-card calls reject with ordinary codes. They used the call's name (ReactNativeBlobUtil.createMediaFile) or, forcopyToInternal, the whole message as the code: an invalid descriptor or media type isEINVAL, a missing sourceENOENT, a provider refusalEACCES, anything elseEUNSPECIFIED.copyToInternalalso settles once; after a failure it used to reject and then resolve"". -
ReactNativeBlobUtilUtils.sharedTrustManageris still a static field. Java and Kotlin code that sets it compiles unchanged. -
ReactNativeBlobUtilFileTransformerstays a Java class, so existingFileTransformerimplementations in Java or Kotlin keep compiling, whatever nullability a Kotlin implementation declared. -
ReactNativeBlobUtilPackagekeeps its name and no-argument constructor, for apps that register it by hand. -
The library no longer applies
kotlin-androidwhen the app already provides Kotlin (AGP 9 withandroid.builtInKotlin=true). -
The library no longer depends on
commons-lang3. If your app used it without declaring it, add it to your own dependencies. -
ReactNativeBlobUtilReq.enableTls12OnPreLollipopreturns the builder unchanged. It only ever did something on Android 4.1 to 4.4, whichminSdk24 excludes.
Classes that were package-private in Java (ReactNativeBlobUtilFS,
ReactNativeBlobUtilImpl, ReactNativeBlobUtilConfig, ReactNativeBlobUtilBody) are
now internal Kotlin classes. Code outside the library couldn't use them before either.
Apps that follow the README don't need to change anything.
ReactNativeBlobUtilFileTransformer.hstays Objective-C. TheFileTransformerprotocol and+setFileTransformer:are unchanged and work from an Objective-C or a SwiftAppDelegate.- The pod now contains Swift (
swift_version5.0) and defines a module. CocoaPods handles this under static libraries, static frameworks and dynamic frameworks alike. - The only public headers are
ReactNativeBlobUtilFileTransformer.handReactNativeBlobUtilExceptionCatch.h. The other headers, includingReactNativeBlobUtilConst.h, are gone: those classes are Swift now. App code that imported them has to stop, and apps had no documented reason to. ios/ReactNativeBlobUtil.xcodeprojis deleted. It no longer matched the sources, so linking the library through that project stopped working long ago. Use CocoaPods.
trusty: trueno longer accepts any certificate for any host. The chain is still not validated, but the host name and the validity dates are: a certificate that names another host, or has expired, is refused and the request fails withESSL.pinnedHostsentries match the host case-insensitively; one written with capitals used to match nothing and silently fall back to system trust.fileCacheandkeyresponses are written toLibrary/Application Support/ReactNativeBlobUtil_tmpinstead ofDocuments/ReactNativeBlobUtil_tmp, so they are no longer in the iCloud backup or visible in the Files app. A path to a cached file stored before the upgrade no longer exists; download the file again (or move it out ofDocumentsyourself).fs.hashrejectsEUNSPECIFIEDwhere it rejectedEUNKNOWNandEREAD; the messages are unchanged.
fetchreports the response:res.info()(and sores.status,res.ok,res.headers,res.url) is filled as on Android and iOS, and thestateChangeevent fires. Windows reported nothing, sostatuswas undefined.- Redirects are followed (
followRedirectdefaults totrue, as elsewhere); Windows returned the 3xx response. Each hop gets its own trust decision, socustomCACertsandpinnedHostsapply per host, andAuthorizationis dropped on a move to another host, as OkHttp does. timeoutis applied, in milliseconds, and a timeout rejectsETIMEDOUTwithrespInfo.timeouttrue. It was read as seconds and never used.overwrite: falseappends to an existing file, as on Android and iOS; it was ignored.fetchaccepts every HTTP method. It rejectedPATCH,HEADandOPTIONSwithEINVAL("Method not supported").- Request bodies follow the rules under Request bodies. Before 1.0
Windows only read a file for a bare
file://prefix (sowrap(path)was sent as text), never decoded base64, and read a file for a multipart field even without afilename. A text body without a Content-Type is sent without one, like Android; it used to gettext/plain; charset=UTF-8, and so did every multipart text part, which now defaults totext/plain(application/octet-streamwith afilename).