Skip to main content

Upload

Upload a file directly to R2 via a presigned URL. The server generates the URL, then the browser uploads directly to R2 — the file never passes through the server.

Range Requests

Fetch a byte range from showcase/large/padded.bin (1 MB repeating pattern). R2 supports the Range HTTP header for partial reads.

Size 64 bytes

MIME Enforcement

How presigned URLs lock Content-Type into the signature, preventing type-mismatch attacks.

Match Correct Content-Type

Presigned URL was generated with Content-Type: image/png. Client sends Content-Type: image/png.

200 OK — Upload succeeds

Mismatch Wrong Content-Type

Presigned URL was generated with Content-Type: image/png. Client sends Content-Type: text/html.

403 SignatureDoesNotMatch — Rejected

How It Works

// Server: Content-Type is baked into the signature
const command = new PutObjectCommand({
  Bucket: BUCKET,
  Key: key,
  ContentType: mimeType, // Locked into signature
});
const url = await getSignedUrl(s3, command, {
  expiresIn: 300,
});

// Client: Must send the exact same Content-Type
fetch(url, {
  method: 'PUT',
  headers: { 'Content-Type': mimeType },
  body: file,
});

This prevents XSS attacks where an attacker could upload an HTML file disguised as an image. The signature ensures the Content-Type cannot be changed after the URL is generated.