Skip to content

Progress Tracking

fetchquack provides upload and download progress tracking that works on all platforms with platform-optimized implementations.

Track file upload progress:

import { HttpClient } from 'fetchquack';
const client = new HttpClient();
const fileInput = document.getElementById('file') as HTMLInputElement;
const file = fileInput.files[0];
await client.fetch({
method: 'POST',
url: '/api/upload',
body: file,
headers: {
'Content-Type': file.type
},
onUploadProgress: (progress) => {
console.log(`Uploaded: ${progress.loaded} / ${progress.total} bytes`);
console.log(`Progress: ${progress.percentage}%`);
progressBar.style.width = `${progress.percentage}%`;
progressText.textContent = `${progress.percentage}% uploaded`;
}
});
console.log('Upload complete!');

Track file download progress:

const data = await client.fetch({
method: 'GET',
url: '/api/files/large-file.zip',
decodeToString: false,
onDownloadProgress: (progress) => {
if (progress.total) {
console.log(`Downloaded: ${progress.percentage}%`);
progressBar.style.width = `${progress.percentage}%`;
} else {
// Total unknown (no Content-Length header)
const mb = (progress.loaded / (1024 * 1024)).toFixed(2);
console.log(`Downloaded: ${mb} MB`);
}
}
});

Track both directions for requests that upload data and receive a response:

await client.fetch({
method: 'POST',
url: '/api/process-file',
body: inputFile,
onUploadProgress: (p) => {
uploadBar.style.width = `${p.percentage}%`;
uploadText.textContent = `Uploading: ${p.percentage}%`;
},
onDownloadProgress: (p) => {
downloadBar.style.width = `${p.percentage}%`;
downloadText.textContent = `Downloading: ${p.percentage}%`;
}
});
interface HttpProgressEvent {
loaded: number; // Bytes transferred so far
total?: number; // Total bytes (undefined if Content-Length not available)
percentage?: number; // 0-100 (undefined if total is unknown)
}

Progress tracking works the same in Angular since fetch() returns a Promise:

import { Component, inject, signal } from '@angular/core';
import { NgxHttpClient } from 'fetchquack/ngx';
@Component({
selector: 'app-file-upload',
template: `
<input type="file" (change)="onFileSelected($event)" />
<button (click)="upload()" [disabled]="!selectedFile()">Upload</button>
<div class="progress-bar">
<div class="progress" [style.width.%]="uploadProgress()"></div>
</div>
<div>{{ uploadProgress() }}% uploaded</div>
`
})
export class FileUploadComponent {
private http = inject(NgxHttpClient);
selectedFile = signal<File | null>(null);
uploadProgress = signal(0);
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files?.length) {
this.selectedFile.set(input.files[0]);
}
}
async upload() {
const file = this.selectedFile();
if (!file) return;
this.uploadProgress.set(0);
try {
await this.http.fetch({
method: 'POST',
url: '/api/upload',
body: file,
headers: { 'Content-Type': file.type },
onUploadProgress: (progress) => {
if (progress.percentage !== undefined) {
this.uploadProgress.set(Math.round(progress.percentage));
}
}
});
console.log('Upload complete!');
this.uploadProgress.set(100);
} catch (err) {
console.error('Upload failed:', err);
}
}
}

Track progress when uploading multiple files:

const formData = new FormData();
formData.append('file1', file1);
formData.append('file2', file2);
formData.append('description', 'Multiple files');
await client.fetch({
method: 'POST',
url: '/api/upload-multiple',
body: formData,
// Don't set Content-Type - browser sets it with boundary
onUploadProgress: (progress) => {
console.log(`Uploading: ${progress.percentage}%`);
}
});

Download with progress indicator:

const response = await client.fetch({
method: 'GET',
url: '/api/files/video.mp4',
decodeToString: false,
onDownloadProgress: (progress) => {
if (progress.total) {
const mb = (progress.loaded / (1024 * 1024)).toFixed(1);
const totalMb = (progress.total / (1024 * 1024)).toFixed(1);
progressText.textContent = `${mb} MB / ${totalMb} MB`;
progressBar.style.width = `${progress.percentage}%`;
}
}
});
// Create downloadable link
const blob = new Blob([response], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
downloadLink.href = url;

Progress tracking uses platform-optimized implementations:

PlatformUpload ProgressDownload ProgressImplementation
BrowserFull supportFull supportXMLHttpRequest with progress events
Node.jsChunked streamingFull supportReadableStream monitoring (64KB chunks)
BunChunked streamingFull supportReadableStream monitoring
DenoChunked streamingFull supportReadableStream monitoring

On the server (Node.js/Bun/Deno), upload progress is tracked by sending the body in 64KB chunks via a ReadableStream. If streaming request bodies aren’t supported, a fallback emits 0% and 100% events.

When the server doesn’t send a Content-Length header, total and percentage are undefined:

await client.fetch({
method: 'GET',
url: '/api/stream-data',
onDownloadProgress: (progress) => {
if (progress.percentage !== undefined) {
// Total is known - show percentage
progressBar.style.width = `${progress.percentage}%`;
} else {
// Total is unknown - show bytes downloaded
const kb = (progress.loaded / 1024).toFixed(0);
progressText.textContent = `Downloaded: ${kb} KB`;
progressBar.classList.add('indeterminate');
}
}
});

Avoid updating the UI too frequently:

let lastUpdate = 0;
const THROTTLE_MS = 100;
await client.fetch({
method: 'GET',
url: '/api/large-file',
onDownloadProgress: (progress) => {
const now = Date.now();
if (now - lastUpdate >= THROTTLE_MS) {
updateProgressUI(progress);
lastUpdate = now;
}
}
});
let lastLoaded = 0;
let lastTime = Date.now();
await client.fetch({
method: 'GET',
url: '/api/file',
onDownloadProgress: (progress) => {
const now = Date.now();
const timeDiff = (now - lastTime) / 1000; // seconds
const bytesDiff = progress.loaded - lastLoaded;
if (timeDiff > 0) {
const speed = bytesDiff / timeDiff;
const speedMB = (speed / (1024 * 1024)).toFixed(2);
if (progress.total && speed > 0) {
const remaining = (progress.total - progress.loaded) / speed;
const minutes = Math.floor(remaining / 60);
const seconds = Math.floor(remaining % 60);
console.log(`Speed: ${speedMB} MB/s, ETA: ${minutes}m ${seconds}s`);
}
lastLoaded = progress.loaded;
lastTime = now;
}
}
});

Always handle errors alongside progress tracking:

import { HttpError } from 'fetchquack';
try {
await client.fetch({
method: 'POST',
url: '/api/upload',
body: file,
onUploadProgress: (progress) => {
updateProgress(progress.percentage);
}
});
showSuccess('Upload complete!');
} catch (error) {
if (error instanceof HttpError) {
showError(`Upload failed: ${error.message}`);
}
resetProgress();
}