Upload Large Files in Parts with XMLHttpRequest & Digest Authentication in React Native (iOS/Android)
Uploading large files (e.g., videos, high-res images, or backups) in a single request is fraught with challenges: network timeouts, unstable connections, and security risks. Chunked uploads—splitting files into smaller "chunks" and uploading them sequentially—mitigate these issues by enabling resumable transfers, reducing payload size per request, and improving error handling. When combined with Digest Authentication, a secure alternative to Basic Auth (which sends credentials in plaintext), you ensure sensitive data (like usernames/passwords) is never exposed in transit.
This blog will guide you through implementing chunked file uploads with XMLHttpRequest (XHR) and Digest Authentication in React Native, supporting both iOS and Android. We’ll cover file system access, chunk splitting, Digest Auth challenge-response flows, and handling edge cases like network failures.
Table of Contents#
- Prerequisites
- Understanding the Problem: Why Chunked Uploads & Digest Auth?
- Key Concepts
- 3.1 Chunked File Uploads
- 3.2 Digest Authentication
- Step-by-Step Implementation
- 4.1 Setup Dependencies
- 4.2 Access the File System
- 4.3 Split the File into Chunks
- 4.4 Implement Digest Authentication
- 4.5 Upload Chunks with XMLHttpRequest
- 4.6 Track Progress & Handle Retries
- Handling Edge Cases
- Testing on iOS/Android
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have:
- A React Native project (0.60+ recommended) set up for iOS and Android.
- Basic knowledge of React Native, JavaScript/TypeScript, and HTTP.
- A server that supports:
- Chunked uploads (accepts partial file data with
Content-Rangeheaders). - Digest Authentication (returns
401 Unauthorizedchallenges withWWW-Authenticateheaders).
- Chunked uploads (accepts partial file data with
- Dependencies:
react-native-fs: To read files from the device’s storage.react-native-document-picker: To let users select files.blueimp-md5: To compute MD5 hashes for Digest Auth (required for the challenge-response flow).
Understanding the Problem: Why Chunked Uploads & Digest Auth?#
The Limitations of Single-Request Uploads#
- Network Instability: A 1GB file upload will fail entirely if the network drops, forcing a full reupload.
- Server Timeouts: Servers often cap request size/duration, blocking large files.
- Memory Overhead: Loading entire files into memory risks app crashes (OOM errors) on low-end devices.
Why Digest Authentication?#
- Security: Unlike Basic Auth (sends base64-encoded credentials), Digest Auth uses a challenge-response mechanism. Credentials are never sent in plaintext—instead, a hash of the username, password, and server-provided nonce is transmitted.
- Compliance: Ideal for apps handling sensitive data (e.g., healthcare, finance) where Basic Auth is forbidden.
Key Concepts#
3.1 Chunked File Uploads#
Chunked uploads split a file into smaller segments (e.g., 5MB chunks) and upload them individually. The server stores chunks temporarily and reassembles the file once all chunks are received.
Workflow:
- Determine the file size and split it into
Nchunks (e.g., a 20MB file with 5MB chunks = 4 chunks). - For each chunk, upload it with metadata (e.g.,
Content-Range: bytes 0-5242879/20971520for the first 5MB of a 20MB file). - The server acknowledges successful chunks. If a chunk fails, only that chunk is reuploaded.
3.2 Digest Authentication#
Digest Auth uses a challenge-response flow to verify credentials without sending passwords:
- Client Sends Request: The client sends an upload request without authentication.
- Server Challenges: The server responds with
401 Unauthorizedand aWWW-Authenticateheader containing:realm: A description of the protected area (e.g., "File Upload Service").nonce: A unique server-generated value (prevents replay attacks).qop(Quality of Protection): e.g.,auth(authentication only).
- Client Computes Response: The client calculates a hash using:
HA1 = MD5(username:realm:password)HA2 = MD5(HTTP_METHOD:request_uri)response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
(Wherenc= nonce count,cnonce= client-generated nonce.)
- Client Retries with Auth Header: The client resends the request with an
Authorizationheader containing the computedresponse.
Step-by-Step Implementation#
4.1 Setup Dependencies#
Install required libraries:
# File system access
npm install react-native-fs --save
# File picker
npm install react-native-document-picker --save
# MD5 hashing for Digest Auth
npm install blueimp-md5 --save
# Link native modules (for React Native < 0.60; auto-linked in 0.60+)
cd ios && pod install && cd ..4.2 Access the File System#
First, let users select a file and fetch its metadata (path, size). Use react-native-document-picker to pick files and react-native-fs to read file details.
Example: Select a File and Get Metadata#
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
const selectFile = async () => {
try {
const result = await DocumentPicker.pick({
type: [DocumentPicker.types.allFiles], // Allow any file type
});
// Get file stats (size, path)
const fileStats = await RNFS.stat(result.uri);
const fileInfo = {
uri: result.uri,
name: result.name,
size: fileStats.size, // Total file size in bytes
path: result.uri.replace('file://', ''), // RNFS uses paths without file://
};
return fileInfo;
} catch (err) {
if (DocumentPicker.isCancel(err)) {
console.log('User canceled file picker');
} else {
throw err;
}
}
};4.3 Split the File into Chunks#
To avoid loading the entire file into memory, read chunks sequentially using react-native-fs. Define a chunk size (e.g., 5MB = 5 * 1024 * 1024 bytes).
Example: Split File into Chunks#
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
const splitFileIntoChunks = async (fileInfo) => {
const { path, size } = fileInfo;
const totalChunks = Math.ceil(size / CHUNK_SIZE);
const chunks = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min((i + 1) * CHUNK_SIZE, size);
const chunkSize = end - start;
// Use RNFS.read() to read specific bytes with position and length
// Parameters: path, length, position, encoding
const chunkBase64 = await RNFS.read(path, chunkSize, start, 'base64');
// Convert base64 to Uint8Array for binary upload
const chunkData = base64ToUint8Array(chunkBase64);
chunks.push({
index: i,
start,
end,
size: chunkSize,
data: chunkData,
});
}
return chunks;
};
// Helper: Convert base64 to Uint8Array (implemented without atob for React Native)
const base64ToUint8Array = (base64) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const len = base64.length;
const bytes = [];
for (let i = 0; i < len; i += 4) {
const b1 = base64.charAt(i) === '=' ? 0 : chars.indexOf(base64.charAt(i));
const b2 = base64.charAt(i + 1) === '=' ? 0 : chars.indexOf(base64.charAt(i + 1));
const b3 = base64.charAt(i + 2) === '=' ? 0 : chars.indexOf(base64.charAt(i + 2));
const b4 = base64.charAt(i + 3) === '=' ? 0 : chars.indexOf(base64.charAt(i + 3));
bytes.push((b1 << 2) | (b2 >> 4));
if (base64.charAt(i + 2) !== '=') bytes.push(((b2 & 15) << 4) | (b3 >> 2));
if (base64.charAt(i + 3) !== '=') bytes.push(((b3 & 3) << 6) | b4);
}
return new Uint8Array(bytes);
};4.4 Implement Digest Authentication#
To handle Digest Auth, we need to:
- Detect
401 Unauthorizedresponses. - Parse the
WWW-Authenticateheader to extractrealm,nonce,qop, etc. - Compute the
responsehash using the user’s credentials and server challenge.
Step 4.4.1 Parse the Digest Challenge#
When the server returns a 401, extract parameters from the WWW-Authenticate header:
import md5 from 'blueimp-md5';
const parseDigestChallenge = (wwwAuthenticateHeader) => {
// Example header: 'Digest realm="FileUpload", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", qop="auth", algorithm=MD5'
const params = {};
const parts = wwwAuthenticateHeader.replace('Digest ', '').split(',');
parts.forEach(part => {
const [key, value] = part.trim().split('=');
params[key] = value.replace(/"/g, ''); // Remove quotes (e.g., "realm" → realm)
});
return params; // { realm: "FileUpload", nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093", qop: "auth", ... }
};Step 4.4.2 Compute the Digest Response#
Use the parsed challenge and user credentials to compute the response hash:
const computeDigestAuthHeader = ({
method, // e.g., 'PUT'
uri, // e.g., '/upload/chunk?fileId=123&chunk=0'
username,
password,
challenge, // Parsed from WWW-Authenticate
}) => {
const { realm, nonce, qop, algorithm = 'MD5' } = challenge;
const cnonce = Math.random().toString(36).substring(2, 12); // Client nonce (random string)
const nc = '00000001'; // Nonce count (increment for retries)
// Compute HA1: MD5(username:realm:password)
const HA1 = md5(`${username}:${realm}:${password}`);
// Compute HA2: MD5(method:uri)
const HA2 = md5(`${method}:${uri}`);
// Compute response: MD5(HA1:nonce:nc:cnonce:qop:HA2)
const response = md5(`${HA1}:${nonce}:${nc}:${cnonce}:${qop}:${HA2}`);
// Assemble Authorization header
return `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${uri}", qop=${qop}, nc=${nc}, cnonce="${cnonce}", response="${response}", algorithm=${algorithm}`;
};4.5 Upload Chunks with XMLHttpRequest#
Now, upload each chunk using XMLHttpRequest (XHR), which supports progress tracking and binary data uploads. We’ll handle Digest Auth challenges and retry failed requests.
Step 4.5.1 Upload a Single Chunk#
Define a function to upload one chunk with XHR, including Digest Auth handling:
const uploadChunk = async ({
chunk, // Chunk object from Step 4.3
fileId, // Unique ID for the file (e.g., generated client-side)
username,
password,
serverUrl, // e.g., 'https://your-server.com/upload/chunk'
}) => {
const { index, start, end, size, data } = chunk;
const chunkUri = `${serverUrl}?fileId=${fileId}&chunkIndex=${index}`;
const method = 'PUT';
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, chunkUri);
// Set headers (excluding Authorization initially)
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
xhr.setRequestHeader('Content-Length', size.toString());
xhr.setRequestHeader('Content-Range', `bytes ${start}-${end - 1}/${totalFileSize}`);
// Track progress (optional but useful for UI)
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const progress = (e.loaded / e.total) * 100;
console.log(`Chunk ${index} progress: ${progress.toFixed(2)}%`);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({ chunkIndex: index, success: true });
} else if (xhr.status === 401) {
// Handle Digest Auth challenge
handleDigestChallengeAndRetry(xhr, resolve, reject);
} else {
reject(new Error(`Chunk ${index} failed: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error(`Network error for chunk ${index}`));
xhr.ontimeout = () => reject(new Error(`Timeout for chunk ${index}`));
// Send the chunk data (Uint8Array)
xhr.send(data);
// Helper: Handle 401 and retry with Digest Auth
async function handleDigestChallengeAndRetry(xhr, resolve, reject) {
try {
const wwwAuthenticate = xhr.getResponseHeader('WWW-Authenticate');
if (!wwwAuthenticate || !wwwAuthenticate.startsWith('Digest ')) {
reject(new Error('Not a Digest Auth challenge'));
return;
}
// Parse challenge and compute Authorization header
const challenge = parseDigestChallenge(wwwAuthenticate);
const authHeader = computeDigestAuthHeader({
method,
uri: chunkUri,
username,
password,
challenge,
});
// Retry the request with the Authorization header
const retryXhr = new XMLHttpRequest();
retryXhr.open(method, chunkUri);
retryXhr.setRequestHeader('Authorization', authHeader);
retryXhr.setRequestHeader('Content-Type', 'application/octet-stream');
retryXhr.setRequestHeader('Content-Length', size.toString());
retryXhr.setRequestHeader('Content-Range', `bytes ${start}-${end - 1}/${totalFileSize}`);
retryXhr.onload = () => {
if (retryXhr.status >= 200 && retryXhr.status < 300) {
resolve({ chunkIndex: index, success: true });
} else {
reject(new Error(`Digest Auth failed for chunk ${index}: ${retryXhr.statusText}`));
}
};
retryXhr.onerror = () => reject(new Error(`Retry failed for chunk ${index}`));
retryXhr.send(data);
} catch (err) {
reject(err);
}
}
});
};Step 4.5.2 Upload All Chunks Sequentially#
Upload chunks one at a time (or in parallel, with concurrency limits) and track overall progress:
const uploadAllChunks = async ({
chunks,
fileId,
username,
password,
serverUrl,
totalFileSize,
}) => {
const uploadedChunks = [];
const failedChunks = [];
for (const chunk of chunks) {
try {
console.log(`Uploading chunk ${chunk.index}...`);
await uploadChunk({
chunk,
fileId,
username,
password,
serverUrl,
totalFileSize,
});
uploadedChunks.push(chunk.index);
} catch (err) {
console.error(`Failed to upload chunk ${chunk.index}:`, err);
failedChunks.push(chunk.index);
}
}
return { uploadedChunks, failedChunks };
};4.6 Track Progress & Handle Retries#
To improve UX, track global upload progress and retry failed chunks:
// Track global progress (e.g., for a progress bar)
const [uploadProgress, setUploadProgress] = useState(0);
// After uploading chunks:
const { uploadedChunks, failedChunks } = await uploadAllChunks(...);
const totalChunks = chunks.length;
const progress = (uploadedChunks.length / totalChunks) * 100;
setUploadProgress(progress);
// Retry failed chunks (e.g., with exponential backoff)
if (failedChunks.length > 0) {
console.log(`Retrying ${failedChunks.length} failed chunks...`);
const retryChunks = chunks.filter(chunk => failedChunks.includes(chunk.index));
await uploadAllChunks({ ...params, chunks: retryChunks });
}Handling Edge Cases#
Network Failures#
- Detect Interruptions: Use
xhr.onerrorandxhr.ontimeoutto catch network issues. - Exponential Backoff: Retry failed chunks with increasing delays (e.g., 1s, 2s, 4s) to avoid overwhelming the server.
Resuming Uploads#
- Check Uploaded Chunks: Add a server endpoint (e.g.,
/upload/status?fileId=xxx) to return indices of already uploaded chunks. Skip these in future uploads.
Memory Management#
- Avoid Loading Entire Files: Use
react-native-fs’sreadFilewithoffsetandlengthto read chunks incrementally (prevents OOM errors).
Digest Auth Edge Cases#
- Nonce Expiry: If the server’s
nonceexpires, the client will receive a new401challenge. Re-parse theWWW-Authenticateheader and recompute theresponse.
Testing on iOS/Android#
iOS#
- Use the iOS Simulator or a physical device. Enable "Network Link Conditioner" (Settings → Developer) to simulate slow/unstable networks.
- Verify file access permissions in
Info.plist(addNSPhotoLibraryUsageDescriptionif accessing photos).
Android#
- Test on an emulator or device with Android 7.0+ (API 24+).
- Ensure
AndroidManifest.xmlincludesINTERNETpermission:<uses-permission android:name="android.permission.INTERNET" /> - Use
adb logcatto debug network requests and native module issues.
Conclusion#
Chunked uploads with XMLHttpRequest and Digest Authentication provide a robust, secure way to handle large files in React Native. By splitting files into chunks, you enable resumable transfers and reduce memory usage, while Digest Auth ensures credentials remain secure.
For further improvements, consider:
- Using background uploads (React Native’s
BackgroundTasksor native modules likereact-native-background-upload). - Adding checksum validation (e.g., SHA-256) to ensure chunk integrity.
- Supporting parallel chunk uploads (with concurrency limits to avoid overwhelming the server).