Ga naar inhoud
HexaTransfer
Terug naar blog
Technische verdiepingen

Hervatbare chunked file upload implementeren in JavaScript

Implementeer hervatbare chunked uploads in JavaScript. Verwerk grote bestanden, volg voortgang en herstel na netwerkonderbrekingen zonder dataverlies.

Het NCSC (Nationaal Cyber Security Centrum) waarschuwt regelmatig voor datalekken als gevolg van afgebroken overdrachten waarbij gevoelige bestanden slechts gedeeltelijk werden verstuurd. Chunked file upload in JavaScript lost dat probleem op aan de basis: splits een groot bestand in stukken van vaste omvang (doorgaans 5-10 MB), upload elk stuk als een afzonderlijk HTTP-verzoek en herassembleer ze op de server. Het patroon lost drie concrete problemen op: browsers en proxies beëindigen verzoeken boven 2 GB, mobiele netwerken verbreken verbindingen midden in een upload, en gebruikers willen voortgangsfeedback. Een werkende implementatie gebruikt File.slice() om stukken uit te snijden, fetch met een AbortSignal per stuk, serverassemblage via S3-multipart of een eigen assembler, en een lokale index in IndexedDB zodat hervatting een tabblad-herlaad overleeft.

Waarom stukken beter zijn dan een enkele upload

Een bestand van 4 GB als één verzoek uploaden mislukt om voorspelbare redenen: Nginx's standaard client_max_body_size is 1 MB, Cloudflare begrenst gratis-tier-uploads tot 100 MB per verzoek, AWS API Gateway stopt hard bij 10 MB, en mobile Safari laat tabs crashen die een ArrayBuffer van 4 GB in het geheugen houden. Chunked uploads ontwijken al die plafonds. U krijgt ook voortgangsbalkjes die werkelijk bewegen, herhalingen die niet opnieuw vanaf nul beginnen, en de mogelijkheid om te pauzeren en te hervatten. De keerzijde is meer server-side staat en meer roundtrips — ruwweg één HTTP-verzoek per 5 MB, wat bij een bestand van 10 GB neerkomt op 2.000 verzoeken.

De juiste stukgrootte kiezen

Stukgrootte is een afweging tussen doorvoer en veerkracht. Te klein (onder 1 MB) en u besteedt meer tijd aan TLS-handshakes dan aan data. Te groot (boven 100 MB) en een verbroken verbinding kost minuten upload-tijd. De ideale zone voor de meeste netwerken is 5-10 MB, wat overeenkomt met het 5 MB-minimum van S3-multipart en goed past bij typische TCP-venstergroottes na slow-start.

Meet eerst het netwerk van de gebruiker:

const downlink = navigator.connection?.downlink ?? 10;
const chunkSize = downlink > 20 ? 10 * 1024 * 1024 : 5 * 1024 * 1024;

Op een verbinding van 100 Mbit voltooit een stuk van 10 MB zich in circa een seconde. Op 4G geven stukken van 5 MB een betere herstelkans bij tunnels.

Het bestand snijden en hashen

File.slice() retourneert een Blob die verwijst naar dezelfde schijfbytes zonder te kopiëren, zodat het snijden van een bestand van 20 GB niets kost:

function* sliceFile(file, chunkSize) {
  for (let offset = 0; offset < file.size; offset += chunkSize) {
    yield {
      index: Math.floor(offset / chunkSize),
      blob: file.slice(offset, offset + chunkSize),
      start: offset,
      end: Math.min(offset + chunkSize, file.size)
    };
  }
}

Bereken een SHA-256-hash van elk stuk voor het uploaden zodat de server de integriteit kan verifiëren:

const buffer = await chunk.blob.arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', buffer);
const hash = Array.from(new Uint8Array(digest))
  .map(b => b.toString(16).padStart(2, '0')).join('');

Voor 10 GB data voegt hashen misschien twintig seconden toe op een moderne laptop — zeker de moeite waard om stille corruptie op instabiele mobiele verbindingen te ondervangen.

Uploaden met gecontroleerde gelijktijdigheid

Opeenvolgende uploads verspillen bandbreedte; onbeperkte parallellisatie laat de browser vastlopen. Een limiet van 3-4 gelijktijdige stukken balanceert beide:

async function uploadAll(file, sessionId) {
  const queue = [...sliceFile(file, 5 * 1024 * 1024)];
  const workers = Array.from({ length: 4 }, async () => {
    while (queue.length) {
      const chunk = queue.shift();
      await uploadChunk(chunk, sessionId);
      emitProgress(chunk.index);
    }
  });
  await Promise.all(workers);
}

Elke uploadChunk-aanroep is een PUT /upload/:sessionId/:index met de blob als body en de hash in een header. Gebruik AbortController per stuk zodat u afzonderlijke verzoeken kunt annuleren zonder de hele batch te stoppen.

Opnieuw proberen zonder de server te overbelasten

Netwerkfouten vragen om exponentiële terugval, geen strakke herhaallussen. Een redelijk beleid: 3 pogingen, basisvertraging 500 ms, jitter tot 50%:

async function uploadChunk(chunk, sessionId, attempt = 0) {
  try {
    const res = await fetch(`/upload/${sessionId}/${chunk.index}`, {
      method: 'PUT', body: chunk.blob, headers: { 'X-Hash': chunk.hash }
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
  } catch (e) {
    if (attempt >= 3) throw e;
    const delay = 500 * 2 ** attempt + Math.random() * 250;
    await new Promise(r => setTimeout(r, delay));
    return uploadChunk(chunk, sessionId, attempt + 1);
  }
}

Behandel 5xx-reacties als herstelbaar, 4xx als fataal (behalve 408 en 429). Bij 429 respecteert u de Retry-After-header in plaats van uw eigen terugval.

Hervatten na een tabblad-herlaad

Bewaar de uploadstatus in IndexedDB na elk succesvol stuk:

await db.put('uploads', {
  sessionId, fileName: file.name, fileSize: file.size,
  completedChunks: [...completedSet], updatedAt: Date.now()
}, sessionId);

Wanneer de gebruiker de pagina heropent met dezelfde bestandskiezer, vergelijkt u de size, lastModified en naam van het bestand met opgeslagen sessies. Vraag bij een treffer aan de server welke stukken hij al heeft ontvangen (een eenvoudige GET /upload/:sessionId/status die een bitmap retourneert werkt goed), en upload vervolgens alleen de ontbrekende stukken. Het tus.io-protocol formaliseert precies dit patroon met de Upload-Offset-header; de tus-js-client-bibliotheek biedt een solide implementatie als u niets zelf wilt bouwen.

Stukken samenvoegen op de server

Twee serieuze opties: S3-multipart-upload, waarbij elk stuk een PartNumber wordt en een afsluitende CompleteMultipartUpload ze samenstelt, of een eigen assembler die elk stuk naar een tijdelijk bestand schrijft en ze aan het eind samenvoegt. S3-multipart is goedkoper op schaal omdat u nooit egress betaalt tijdens assemblage. De eigen aanpak is eenvoudiger te debuggen en laat u stroom-encryptie toepassen tijdens assemblage.

const upload = await s3.createMultipartUpload({ Bucket, Key });
// per stuk: s3.uploadPart({ UploadId, PartNumber, Body })
await s3.completeMultipartUpload({ UploadId, MultipartUpload: { Parts } });

Let op de limiet van 10.000 onderdelen — voor bestanden boven 50 GB hebt u stukken van minimaal 5 MB nodig om daaronder te blijven.

Voortgang tonen die gebruikers vertrouwen

Voortgangsbalkjes die springen voelen gebroken. Bereken voortgang als geüploade bytes gedeeld door totale bytes, niet als voltooide stukken, en smooth het met een voortschrijdend gemiddelde over twee seconden om jitter te verbergen. Gebruik fetch met een ReadableStream en een Transform om bytes te tellen, aangezien XMLHttpRequest.upload.onprogress niet altijd betrouwbaar afvuurt op HTTP/3. Toon een geschatte resterende tijd door resterende bytes te delen door de lopende doorvoer, maar begrens de weergave tot minimaal vijf seconden.

Veelgemaakte fouten vermijden

Drie fouten ruïneren chunked uploads in productie: vergeten Content-Length per stuk in te stellen (dit breekt sommige edge-proxies), dezelfde sessie-ID voor verschillende bestanden hergebruiken (dit beschadigt de assemblage), en de gebruiker het bestand laten wijzigen midden in een upload zonder de sessie te versie-beheren. Hash altijd de eerste 1 MB van het bestand plus de grootte en lastModified om sessies te identificeren. Vertrouw lastModified nooit alleen — macOS Finder werkt het bij bij metadatawijzigingen.

HexaTransfer gebruikt een chunked + hervattable pipeline zoals deze voor uploads tot 10 GB, met client-side AES-256-GCM toegevoegd aan elk stuk voor het PUT-verzoek. Probeer het op https://hexatransfer.com — gratis, geen account vereist, maximaal 10 GB.

Alles samengebracht

Een productie-waardige chunked uploader omvat circa 300 regels JavaScript: snijden met File.slice, hashen met SubtleCrypto, 3-4 stukken parallel uploaden met exponentiële terugval, sessiestatus bewaren in IndexedDB, en de server stukken laten samenvoegen via S3-multipart of een eigen assembler. Test het tegen vliegtuigmodus-wisselingen, tabblad-herladen en een bestand van 15 GB op 4G voordat u het vertrouwt. Zodra dat werkt, zijn versleuteling, voortgang en hervatbaarheid slechts extra lagen op hetzelfde skelet.

Verstuur grote bestanden veilig met end-to-end-versleuteling

Draag bestanden tot 10 GB gratis over met end-to-end-versleuteling. Geen account nodig. Uw bestanden worden in uw browser versleuteld voordat ze worden geüpload — niemand anders kan ze lezen.

Een bestand verzenden