Skip to main content

Resumable Upload (tus-style)

Upload a file in chunks, survive an interruption, and resume from the server-reported offset instead of starting over.

EXPERT

🔗 Endpoint

GET /api/v1/upload/resumableOpen in browser ↗

💡 Tips

  • POST with Upload-Length opens a session — read the id from the Location header
  • HEAD returns the authoritative Upload-Offset; never trust your own byte count after a failure
  • PATCH with a stale Upload-Offset is rejected with 409 — re-read the offset and continue from there
  • Exceeding the declared Upload-Length returns 413; DELETE cancels the session
  • Session state is per-instance and expires after 10 minutes — a resumed test must tolerate a cold session

💻 Code Example

// Playwright: upload 20 bytes in two chunks, then prove resume works
const create = await request.post('/api/v1/upload/resumable', {
  headers: { 'Upload-Length': '20' },
});

expect(create.status()).toBe(201);

const id = new URL(create.headers()['location'], 'http://x').searchParams.get('id');

// First chunk
const first = await request.patch(`/api/v1/upload/resumable?id=${id}`, {
  headers: { 'Upload-Offset': '0', 'Content-Type': 'application/offset+octet-stream' },
  data: 'ABCDEFGHIJ',
});

expect(first.headers()['upload-offset']).toBe('10');

// Replaying the same offset is a stale write — the server refuses it
const stale = await request.patch(`/api/v1/upload/resumable?id=${id}`, {
  headers: { 'Upload-Offset': '0' },
  data: 'XXXX',
});

expect(stale.status()).toBe(409);

// Ask the server where to resume, then finish
const head = await request.head(`/api/v1/upload/resumable?id=${id}`);
const offset = head.headers()['upload-offset'];

const second = await request.patch(`/api/v1/upload/resumable?id=${id}`, {
  headers: { 'Upload-Offset': offset, 'Content-Type': 'application/offset+octet-stream' },
  data: 'KLMNOPQRST',
});

expect(second.headers()['upload-offset']).toBe('20');

🖥️ curl Command

curl -O "/api/v1/upload/resumable"