OpenSpace's background HTTP file uploader for React Native. On iOS it uses a
background URLSession; on Android it uses WorkManager (a CoroutineWorker)
with OkHttp. Uploads continue while the app is backgrounded and resume after it
is killed.
Requires React Native ≥ 0.84 with the New Architecture enabled, and React ≥ 19. This is a codegen TurboModule; it does not support the legacy bridge.
yarn add react-native-background-upload
cd ios && pod install && cd ..
pod install is required after installing — it runs codegen to generate the native
spec this module implements.
The package ships TypeScript source with no build step, so it resolves through Metro (and
tsc) but not through plain Node. If you import it from a non-Metro context — a script, or Jest without a transform — add it to yourtransformIgnorePatternsallowlist or mock it.
So uploads that finish while the app is terminated can relaunch it and be
journaled, add this to your AppDelegate:
#import <react_native_background_upload/react_native_background_upload-Swift.h>
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)(void))completionHandler {
[RNBackgroundUpload setBackgroundSessionCompletionHandler:completionHandler
forIdentifier:identifier];
}The Swift header import name is the pod name with hyphens as underscores. If your app links pods as frameworks, use
@import react_native_background_upload;instead of the#import <...-Swift.h>line.
This hook is load-bearing beyond just calling the completion handler: it is what
brings the library's background URLSession back to life in a process the system
relaunched with no JS running, so queued completions get journaled. RNFileUploader
is the TurboModule and is deliberately not reachable from plain Objective-C — its
generated header is Objective-C++ only — so the handler lives on RNBackgroundUpload.
import Upload from 'react-native-background-upload';
const options = {
url: 'https://myservice.com/path/to/post',
path: 'file://path/to/file/on/device',
method: 'POST',
type: 'raw',
headers: { 'content-type': 'application/octet-stream' },
// Optional. Treat these non-2xx statuses as success (e.g. an idempotent
// create that conflicts). Any other non-2xx is an 'error' with errorKind 'http'.
acceptStatus: [409],
// Optional on Android — the library supplies notification defaults and creates
// its own channel. Override any of these to customize.
android: { notificationTitle: 'Uploading…' },
};
const uploadId = await Upload.startUpload(options);
Upload.addListener('progress', uploadId, ({ progress }) => {});
Upload.addListener('completed', uploadId, ({ responseCode, responseBody }) => {});
Upload.addListener('error', uploadId, ({ error, errorKind, responseCode }) => {});
Upload.addListener('cancelled', uploadId, ({ cancelReason }) => {});Terminal events (completed / error / cancelled) are journaled natively
before they are emitted, so they survive app death, JS reloads, and background
relaunches. Events stay in the journal until you acknowledge them. Drain it on
every app start:
const events = await Upload.getUnacknowledgedEvents();
for (const e of events) {
// e: { eventId, id, type, timestamp, responseCode?, responseBody?,
// responseHeaders?, error?, errorKind?, cancelReason? }
handleOutcome(e);
}
await Upload.ackEvents(events.map((e) => e.eventId));
// Then reconcile anything still in flight:
const live = await Upload.getAllUploads(); // [{ id, state, ... }]Notes:
completedfires only for 2xx (or a request'sacceptStatus). Every other HTTP response is anerrorwitherrorKind: 'http'and the response attached — a 400 is an error, not a completion.errorKindis'http' | 'network' | 'file' | 'unknown'. Retry transport failures; treat client errors as terminal.cancelReasondistinguishes a user cancel ('user') from a system kill ('system').- Duplicate journal entries for one upload id are possible if the process dies at
the wrong moment (Android may re-run the worker) — dedupe by
id, keep latest. - Android:
getAllUploads()reflects only live/recent work (WorkManager prunes finished work after ~a day). The journal is the source of truth for outcomes.
All methods are on the default export.
Starts an upload; resolves to its id. Rejects only on a bad option (missing/invalid
url or path) — transport failures and HTTP error responses arrive later as
error events, not a rejection.
| Option | Type | Notes |
|---|---|---|
url |
string | Required. |
path |
string | Required. Local file path (file://…). URIs are not escaped for you. |
type |
'raw' |
Only raw is supported. |
method |
string | Default POST. |
headers |
object | HTTP headers. |
customUploadId |
string | Defaults to a generated UUID. |
wifiOnly |
boolean | Wait for wifi before/while uploading. |
acceptStatus |
number[] | Non-2xx statuses to treat as success. |
android |
object | Optional. notificationId/Title/TitleNoWifi/TitleNoInternet/Channel, maxRetries (default 5). Sensible defaults + auto-created channel if omitted. |
Cancels an upload. Fires a cancelled event with cancelReason: 'user'.
Listen for 'progress' | 'error' | 'completed' | 'cancelled'. Pass null for
uploadId to receive events for all uploads. Call .remove() on the result to
unsubscribe.
Terminal events not yet acknowledged, including ones that fired while JS was dead.
Removes journaled events once processed.
Uploads the OS still knows about, for boot-time reconciliation.
iOS-only live task state (running | suspended | canceling, plus byte counts), or
undefined if the task isn't active.
Fires when the Android progress notification is pressed. No event data.
| Event | Data |
|---|---|
progress |
{ id, progress: 0-100 } |
completed |
{ id, responseCode, responseBody, responseHeaders?, eventId? } |
error |
{ id, error, errorKind?, responseCode?, responseBody?, responseHeaders? } |
cancelled |
`{ id, cancelReason?: 'user' |
See CONTRIBUTING.md.