Read, write, and stream objects

Put, get, and authorize objects

Write objects with metadata, handle missing keys, and authorize every operation before using the requested key.

8 minute lesson

~~~

The bucket binding gives you four verbs that cover most applications: put, get, head, and delete.

Use put to store the request body or another stream, and set accurate content metadata while you’re at it:

await env.FILES.put(`tenant/${tenantId}/uploads/${id}.pdf`, request.body, {
  httpMetadata: { contentType: 'application/pdf' },
})

The second argument can be a stream, an ArrayBuffer, a string, or a Blob. Passing request.body directly streams the upload into R2 without holding it in Worker memory.

Use get for the body and head when only metadata is required. head is the cheaper call when you want to know whether an object exists or how big it is, without transferring the bytes.

get returns null for a missing key. Handle it and return 404:

const object = await env.FILES.get(key)

if (!object) {
  return new Response('Not found', { status: 404 })
}

const headers = new Headers()
object.writeHttpMetadata(headers)
return new Response(object.body, { headers })

writeHttpMetadata copies the stored content type onto the response, so the browser knows what it received.

Authorization is your job

Here’s the part people get wrong. A private bucket does not automatically supply your application’s tenant authorization. The bucket being private only means anonymous internet traffic can’t reach it. Every request that arrives at your Worker can reach it, through your code.

So verify the authenticated user owns the logical key before reading, replacing, listing, or deleting it:

const key = `tenant/${session.tenantId}/uploads/${filename}`

if (filename.includes('/') || filename.includes('..')) {
  return new Response('Invalid name', { status: 400 })
}

Build the prefix from the session, never from the request. The client only supplies the final filename segment, and you reject anything shaped like a path.

Test this the way an attacker would. Request ../../tenant/456/uploads/secret.pdf as a filename and confirm you get a 400, not another tenant’s file. One curl with a hostile key tells you more than a week of happy-path testing.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →