You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Android, downloadUpdate crashes the whole app with an uncaught NullPointerException thrown inside revopush-diff (org.revopush.ota.utils.FileUtils.copyDirectoryContents, "Attempt to get length of null array"). Because the exception escapes CodePushNativeModule$3.doInBackground, AsyncTask rethrows it as a fatal RuntimeException, and since the failed package is never recorded, the same diff download is retried and crashes again on every app launch. Affected devices are stuck in a crash loop until the app data is cleared.
We see it in production (Sentry, ~1,050 events / 30 users over 30 days, up to ~110 events per device per week) across all our Android store builds since 2026-05.
react-native 0.79.6, New Architecture (fabric + bridgeless), Hermes
Android 12–16, various Samsung devices (no device/OS skew)
Server: Revopush cloud; releases created with revopush release-expo (CLI 0.0.13)
Stack trace
java.lang.RuntimeException: An error occurred while executing doInBackground()
at android.os.AsyncTask$4.done(AsyncTask.java:415)
...
Caused by: java.lang.NullPointerException: Attempt to get length of null array
at org.revopush.ota.utils.FileUtils.copyDirectoryContents(SourceFile:12)
at org.revopush.ota.BundleManager.a(SourceFile:51)
at org.revopush.ota.BundleManager.downloadPackage(SourceFile:18)
at com.microsoft.codepush.react.CodePushUpdateManager.downloadPackage(CodePushUpdateManager.java:171)
at com.microsoft.codepush.react.CodePushNativeModule$3.doInBackground(CodePushNativeModule.java:431)
at com.microsoft.codepush.react.CodePushNativeModule$3.doInBackground(CodePushNativeModule.java:425)
at android.os.AsyncTask$3.call(AsyncTask.java:394)
Analysis
revopush-diff has no public source, so this is from the AAR bytecode (javap), checked on both 0.0.7 and the latest 0.0.9 (Maven, 2026-09-03):
BundleManager.downloadPackage takes the diff path when a current package exists and the update has a bundleDiffBlobUrl. Inside the asset-diff step (the private a(UpdatePackage, BiFunction, JSONObject, File, String, String, String, String) overload), the "local asset diff" branch calls FileUtils.copyDirectoryContents(currentPackageFolderPath, newPackageFolderPath, predicate) guarded only by a null check on the path string, not by File.exists().
FileUtils.copyDirectoryContents(String, String, Predicate) does sourceDir.listFiles() and iterates with arraylength directly. listFiles() returns null when the directory does not exist, hence the NPE.
CodePushNativeModule.downloadUpdate only catches CodePushInvalidUpdateException and IOException | CodePushUnknownException, so the NPE is fatal, saveFailedUpdate is never called, and the device retries the same diff on the next launch.
0.0.9 (as pinned by SDK 2.6.1 / 2.5.2) has the same bytecode for both the call site and copyDirectoryContents, so upgrading does not fix it.
We have not yet identified what removes the current package folder while codepush.json still points at it (candidates: OS storage cleanup on low-space devices, as in the Microsoft issue).
In CodePushNativeModule.downloadUpdate, catch RuntimeException as well, so a failure inside the native diff code rejects the promise instead of killing the process. We are shipping this as a patch-package in the meantime:
} catch (RuntimeExceptione) {
CodePushUtils.log(e);
mCodePush.clearUpdates(); // drop the stale package state so the next sync gets a full bundlepromise.reject(e);
}
Happy to provide Sentry event JSON or test a build with a fixed AAR.
Summary
On Android,
downloadUpdatecrashes the whole app with an uncaughtNullPointerExceptionthrown insiderevopush-diff(org.revopush.ota.utils.FileUtils.copyDirectoryContents, "Attempt to get length of null array"). Because the exception escapesCodePushNativeModule$3.doInBackground,AsyncTaskrethrows it as a fatalRuntimeException, and since the failed package is never recorded, the same diff download is retried and crashes again on every app launch. Affected devices are stuck in a crash loop until the app data is cleared.We see it in production (Sentry, ~1,050 events / 30 users over 30 days, up to ~110 events per device per week) across all our Android store builds since 2026-05.
Environment
@revopush/react-native-code-push: 2.5.0-rc.7 (pinsorg.revopush:revopush-diff:0.0.7)revopush release-expo(CLI 0.0.13)Stack trace
Analysis
revopush-diffhas no public source, so this is from the AAR bytecode (javap), checked on both 0.0.7 and the latest 0.0.9 (Maven, 2026-09-03):BundleManager.downloadPackagetakes the diff path when a current package exists and the update has abundleDiffBlobUrl. Inside the asset-diff step (the privatea(UpdatePackage, BiFunction, JSONObject, File, String, String, String, String)overload), the "local asset diff" branch callsFileUtils.copyDirectoryContents(currentPackageFolderPath, newPackageFolderPath, predicate)guarded only by anullcheck on the path string, not byFile.exists().FileUtils.copyDirectoryContents(String, String, Predicate)doessourceDir.listFiles()and iterates witharraylengthdirectly.listFiles()returnsnullwhen the directory does not exist, hence the NPE.copyNecessaryFilesFromCurrentPackagein the same AAR does have theexists()guard (the one Microsoft added in Fix NPE when invokingcopyNecessaryFilesFromCurrentPackage()method on Android microsoft/react-native-code-push#2566 for the identical crash, see also NullPointerException in FileUtils.copyDirectoryContents microsoft/react-native-code-push#1584), but it was not carried over to the new local-asset-diff path.CodePushNativeModule.downloadUpdateonly catchesCodePushInvalidUpdateExceptionandIOException | CodePushUnknownException, so the NPE is fatal,saveFailedUpdateis never called, and the device retries the same diff on the next launch.0.0.9 (as pinned by SDK 2.6.1 / 2.5.2) has the same bytecode for both the call site and
copyDirectoryContents, so upgrading does not fix it.We have not yet identified what removes the current package folder while
codepush.jsonstill points at it (candidates: OS storage cleanup on low-space devices, as in the Microsoft issue).Suggested fix
revopush-diff, guard the local asset diff copy withcurrentPackageFolderPath == null || !new File(currentPackageFolderPath).exists()→ skip the copy and fall through to the existing full-bundle fallback (same as Fix NPE when invokingcopyNecessaryFilesFromCurrentPackage()method on Android microsoft/react-native-code-push#2566 upstream), and/or makecopyDirectoryContentstreat anulllistFiles()as empty.CodePushNativeModule.downloadUpdate, catchRuntimeExceptionas well, so a failure inside the native diff code rejects the promise instead of killing the process. We are shipping this as a patch-package in the meantime:Happy to provide Sentry event JSON or test a build with a fixed AAR.