No Login Data Private Local Save

Fetch Upload Progress Demo - Online Track XHR & Fetch

6
0
0
0

Upload Progress Tracker

Real-time monitoring of XHR & Fetch upload progress with live speed, ETA, and detailed logs

Drag & drop a file here

Tap to select or drop file

or click to browse files

Max 50MB for real upload Β· Any size for simulation

Implementation Reference
function xhrUploadWithProgress(url, file, onProgress) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        
        // Track upload progress via xhr.upload
        xhr.upload.addEventListener('progress', (e) => {
            if (e.lengthComputable) {
                onProgress(e.loaded, e.total);
            }
        });
        
        xhr.addEventListener('load', () => {
            if (xhr.status >= 200 && xhr.status < 300) resolve(xhr);
            else reject(new Error(`HTTP ${xhr.status}`));
        });
        xhr.addEventListener('error', () => reject(new Error('Network error')));
        xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));
        
        xhr.open('POST', url);
        xhr.send(file); // Native progress events fire automatically
    });
}
// Fetch API has NO native upload progress events.
// We wrap the file stream with ReadableStream to track bytes sent.
async function fetchUploadWithProgress(url, file, onProgress, signal) {
    const fileStream = file.stream();
    const reader = fileStream.getReader();
    let uploaded = 0;
    
    const trackingStream = new ReadableStream({
        async pull(controller) {
            const { done, value } = await reader.read();
            if (done) { controller.close(); return; }
            uploaded += value.byteLength;
            onProgress(uploaded, file.size);
            controller.enqueue(value);
        }
    });
    
    const response = await fetch(url, {
        method: 'POST',
        body: trackingStream,
        duplex: 'half',  // Required for streaming body
        signal,
        headers: {
            'Content-Type': file.type || 'application/octet-stream',
            'Content-Length': String(file.size),
        }
    });
    
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response;
}
Frequently Asked Questions
Why doesn't the native Fetch API support upload progress events?
The Fetch API was designed around promises and streams, not event emitters. Unlike XHR which emits progress events on xhr.upload, fetch treats the request body as a stream. To track upload progress, you need to wrap the body stream with a ReadableStream and count bytes as they are read β€” this is the modern, composable approach. Browser vendors opted for stream-based APIs over event-based ones for greater flexibility.
duplex: 'half' tells the browser that the request body is a half-duplex stream β€” data flows in one direction (client β†’ server). This is required when using a ReadableStream as the fetch body. Without it, the browser will throw a TypeError. This option was introduced to support streaming request bodies while maintaining backward compatibility. It's supported in Chrome 105+, Firefox 120+, Safari 17+, and Edge 105+.
XHR is more battle-tested for upload progress tracking. Its xhr.upload.onprogress event is supported in all browsers and gives accurate, real-time progress data. Fetch with ReadableStream wrapping is the modern equivalent but requires newer browser APIs. For production applications targeting all browsers, XHR remains the safer choice. For modern SPAs, the Fetch + ReadableStream pattern works well with proper feature detection.
Yes! For XHR, call xhr.abort() β€” this triggers the abort event. For Fetch, create an AbortController, pass its signal to the fetch options, and call controller.abort() to cancel. When using ReadableStream for upload tracking, the stream's pull() method will stop being called after cancellation. Both methods cleanly terminate the upload.
Speed is calculated using a sliding window of recent samples. Each progress update records (timestamp, bytesUploaded). The tool keeps the last 2 seconds of samples and computes: speed = deltaBytes / deltaTime. This provides a smooth, responsive speed reading that adapts to network fluctuations without excessive jitter. ETA is then derived as remainingBytes / currentSpeed.
For very small files (under ~64KB), the browser may send the entire file in a single chunk. The ReadableStream's pull() is called once, reads the whole file, and progress jumps from 0% to 100% instantly. This is expected behavior. For meaningful progress tracking, files should be at least a few hundred KB. Use the simulation mode in this tool to see granular progress with any file size.
Upload progress tracks data sent from client to server (request body). In XHR, use xhr.upload.onprogress. In Fetch, wrap the request body stream. Download progress tracks data received from server to client (response body). In XHR, use xhr.onprogress. In Fetch, read response.body.getReader() and count bytes. These are separate concerns β€” don't confuse them!
Yes, but with limitations. When using FormData, the browser constructs the multipart body internally. You cannot easily wrap it with a ReadableStream for progress tracking. The best approach is to either: (a) use XHR with FormData (native progress events work), or (b) construct the multipart body manually as a Blob or stream. For simple file uploads, sending the raw file with Content-Type is simpler and fully trackable.