Back to Blog
Lesson 19 of the Cloudflare: Cloudflare for Developers: DNS to CDN course
Cloud NativeJuly 27, 20264 min read

Integrating R2 with Workers: Serving Files at the Edge

Learn how to bind an R2 bucket to your Cloudflare Worker, fetch files programmatically, and handle errors to build a resilient, edge-based file storage system.

CloudflareR2WorkersBindingsStorageIntegration
Neatly arranged blue office binders labeled with dates and names for organized storage.

Previously in this course, we explored Introduction to R2 Storage: Buckets and Object Storage to understand object storage concepts, and in Uploading Files to R2: A Practical Guide for Developers, we mastered manual file uploads via the CLI. In this lesson, we will bridge the gap by connecting your storage directly to your compute layer.

By binding an R2 bucket to a Worker, you gain the ability to manipulate, serve, and secure your files dynamically at the edge without a traditional origin server.

Binding R2 to a Worker

In the Cloudflare ecosystem, a "binding" is a bridge that allows your code to interact with external services (like R2, D1, or KV) using a variable name. Without this binding, your Worker script cannot "see" your bucket.

To bind your bucket, you must modify your wrangler.toml file. This tells the Cloudflare infrastructure which bucket to inject into your Worker’s execution context.

Open your wrangler.toml and add the following block:

TOML
[[r2_buckets]]
binding = 'MY_BUCKET' # The variable name in your code
bucket_name = 'my-app-assets' # The name of your R2 bucket

After saving this change, run npx wrangler deploy to update your environment. Your Worker can now access the R2 API via the env.MY_BUCKET object.

Serving an Image from R2

An overhead shot of a meal setup with laptop, showcasing food and productivity in a cozy indoor setting.

Once the binding is active, retrieving a file is straightforward. You use the get() method provided by the R2 bucket API. Let’s create a simple Worker that serves an image from your bucket based on the request URL.

Replace your index.js (or src/index.ts) content with this:

JAVASCRIPT
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1); // Remove leading slash

    // 1. Fetch the object from R2
    const object = await env.MY_BUCKET.get(key);

    // 2. Handle file retrieval errors
    if (object === null) {
      return new Response(CE9178">'Object Not Found', { status: 404 });
    }

    // 3. Serve the file with appropriate headers
    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set(CE9178">'etag', object.httpEtag);

    return new Response(object.body, {
      headers,
    });
  },
};

Understanding the Mechanics

  • env.MY_BUCKET.get(key): This performs an asynchronous fetch from your storage.
  • object.writeHttpMetadata(headers): This is a powerful helper method. It automatically copies metadata (like Content-Type) you might have set during the upload process to your response, ensuring the browser renders the image correctly.
  • object.body: This is a readable stream, which means your Worker doesn't load the entire file into memory at once. It streams the data directly from R2 to the end user.

Hands-on Exercise: Implementing an Error Fallback

Your task is to improve the error handling in the code above. Currently, if an image is missing, you return a 404. Modify your Worker to return a default image (e.g., placeholder.png) if the requested key does not exist in the bucket.

  1. Create a placeholder.png and upload it to your R2 bucket.
  2. Update your fetch function logic: if object === null, attempt to fetch placeholder.png instead.
  3. If placeholder.png is also missing, return a custom 404 response.

Common Pitfalls

  • Case Sensitivity: R2 keys are case-sensitive. If your file is named Logo.png and your URL requests logo.png, the get() method will return null.
  • Memory Limits: While streaming (object.body) prevents memory exhaustion, avoid trying to use await object.text() or await object.arrayBuffer() on large files. Always stream large assets directly to the response.
  • Binding Mismatches: If your wrangler.toml binding name doesn't match the variable used in your code (e.g., using env.ASSETS while the config says MY_BUCKET), the code will throw an undefined error at runtime.

FAQ

Can I list files in my bucket from a Worker? Yes, you can use env.MY_BUCKET.list(). This is useful for building dynamic galleries or index pages.

Does serving files from R2 through a Worker count as egress? Cloudflare R2 has no egress fees. Serving files via a Worker to the public internet is free of egress charges, regardless of the volume of data.

How do I handle private files? By using a Worker, you can add authentication checks before calling env.MY_BUCKET.get(). This is the standard way to serve private content.

Recap

Wooden Scrabble tiles spelling 'RECAP' on a brown textured background. Text concept image.

In this lesson, we successfully integrated R2 storage into our application by configuring bindings in wrangler.toml and utilizing the R2 API to serve files. We also learned how to leverage streaming for performance and handle missing assets gracefully. This forms the foundation for the file-serving backend of our project.

Up next: Introduction to D1 SQL Database, where we will add a relational database layer to our application to store file metadata and user information.

Similar Posts