Skip to content

[wrangler][miniflare] Serve local R2 bucket objects publicly via /cdn-cgi/local/r2/public#14119

Merged
NuroDev merged 14 commits into
cloudflare:mainfrom
tahmid-23:r2-local-public-bucket
Jun 10, 2026
Merged

[wrangler][miniflare] Serve local R2 bucket objects publicly via /cdn-cgi/local/r2/public#14119
NuroDev merged 14 commits into
cloudflare:mainfrom
tahmid-23:r2-local-public-bucket

Conversation

@tahmid-23

@tahmid-23 tahmid-23 commented May 29, 2026

Copy link
Copy Markdown
Contributor

This implements the idea discussed in #13325.

R2 exposes a public endpoint for accessing buckets remotely, but no such analogue exists in local development.

This adds a configuration parameter, experimental_local_port, that sets up a socket for a given R2 binding. This is sent to a custom worker, which translates a GET/HEAD request into an R2 access. The worker provides full support for range and condition headers.

As a brief aside, I'd like to implement S3-compatible pre-signed uploads in the newly created worker in a follow-up PR.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: I will make a docs PR if this PR is considered for acceptance.

A picture of a cute animal (not mandatory, but encouraged)

@changeset-bot

changeset-bot Bot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 59d440f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
miniflare Minor
wrangler Minor
@cloudflare/pages-shared Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
@cloudflare/wrangler-bundler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@workers-devprod workers-devprod requested review from a team and NuroDev and removed request for a team May 29, 2026 23:08
@workers-devprod

workers-devprod commented May 29, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/r2-local-public-bucket-miniflare.md: [@cloudflare/wrangler]
  • .changeset/r2-local-public-bucket-wrangler.md: [@cloudflare/wrangler]
  • packages/miniflare/README.md: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/r2/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/shared/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/r2/local.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/r2/local.spec.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/environment.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/validation.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/worker.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/MultiworkerRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]

@tahmid-23 tahmid-23 force-pushed the r2-local-public-bucket branch 2 times, most recently from 5de38c1 to 3bd48b7 Compare May 30, 2026 00:19
@NuroDev

NuroDev commented Jun 4, 2026

Copy link
Copy Markdown
Member

Thank you for this PR @tahmid-23.
After speaking with the team on this, we believe the better approach for this change wold be to expose these routes under the /cdn-cgi/mf/r2/... path, similar to the changes made in #13234, rather than exposing a whole new port to serve on.

@tahmid-23

Copy link
Copy Markdown
Contributor Author

@NuroDev makes sense.
Can I implement that myself here, or would you rather it stay under your team?

@NuroDev

NuroDev commented Jun 4, 2026

Copy link
Copy Markdown
Member

Can I implement that myself here, or would you rather it stay under your team?

Feel free to implement that yourself here, and if we can help with anything let us know 😄

@tahmid-23 tahmid-23 force-pushed the r2-local-public-bucket branch from 3bd48b7 to 12cdc58 Compare June 4, 2026 23:26
@workers-devprod

workers-devprod commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/r2-local-public-bucket-miniflare.md: [@cloudflare/wrangler]
  • .changeset/r2-local-public-bucket-wrangler.md: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/r2/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/entry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/r2/public.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/r2/public.spec.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@tahmid-23 tahmid-23 force-pushed the r2-local-public-bucket branch 4 times, most recently from cea5c3f to eca9313 Compare June 5, 2026 00:20
devin-ai-integration[bot]

This comment was marked as resolved.

tahmid-23 and others added 3 commits June 4, 2026 20:28
Accept and validate the experimental_local_public boolean on R2 bucket
bindings in wrangler config. Gated as experimental. No runtime behavior
attached yet — wiring lands in the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the use of the shared `namespaceEntries` helper in the R2 plugin
with a local `r2BucketEntries` typed against `R2OptionsSchema`. No behavior
change — the new helper produces the same shape — but a local helper lets
future R2-specific fields be threaded through without expanding the shared
helper used by other namespace-style plugins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-cgi/mf/r2

Exposes opted-in local R2 buckets at <dev-server>/cdn-cgi/mf/r2/<binding>/<key>
on the existing user-facing dev server, dispatched through the entry worker
(same pattern as the stream binding). Subject to the existing /cdn-cgi/*
Host/Origin allowlist. Enabled per-binding via experimental_local_public: true
on R2 bindings in wrangler config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tahmid-23 tahmid-23 force-pushed the r2-local-public-bucket branch from eca9313 to 85bbe76 Compare June 5, 2026 00:28
@tahmid-23

Copy link
Copy Markdown
Contributor Author

@NuroDev done
I rewrote the history to reduce the code churn

@tahmid-23 tahmid-23 changed the title [wrangler][miniflare] Serve local R2 bucket objects publicly over a dedicated port [wrangler][miniflare] Serve local R2 bucket objects publicly via /cdn-cgi/mf/r2 Jun 5, 2026
Comment thread packages/miniflare/src/plugins/r2/index.ts Outdated
Comment thread .changeset/r2-local-public-bucket-wrangler.md Outdated
Comment thread packages/miniflare/src/plugins/r2/index.ts Outdated
Comment thread packages/miniflare/src/workers/core/constants.ts Outdated
Comment thread packages/miniflare/src/workers/r2/local.worker.ts Outdated
tahmid-23 and others added 4 commits June 9, 2026 12:32
Per review feedback: improves readability when skimming the function.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review feedback: now that the route lives under /cdn-cgi/..., it
can't collide with user routes, so the opt-in flag is no longer needed.
Every local R2 binding is exposed by default (remote-proxied bindings
remain excluded).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review feedback: /cdn-cgi/mf/... is reserved for Miniflare internals.
/cdn-cgi/local/r2/public leaves /cdn-cgi/local/r2/... open for future
local R2-related endpoints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review feedback: matches the local-explorer worker's style and
makes routing easier to maintain. Behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tahmid-23 and others added 2 commits June 9, 2026 13:47
Prod R2 includes Last-Modified (sourced from the object's upload time)
on public-bucket responses. R2Object.writeHttpMetadata() does not emit
it, so set it manually from object.uploaded. Without it, clients
cannot construct correct If-Modified-Since conditional requests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
"public" is a more appropriate name for the worker than "local", since
Miniflare is inherently local.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tahmid-23 tahmid-23 requested a review from NuroDev June 9, 2026 17:52
@tahmid-23

tahmid-23 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

@NuroDev I also added a missing header that the R2 URL sends and renamed the worker to public.worker.ts.
I realize that a small oversight is that this doesn't configure a CORS policy. Do you think I should add a CORS response that allows all origins, or would that be too broad?

devin-ai-integration[bot]

This comment was marked as resolved.

The R2 head operation cannot evaluate conditional headers (the wire
request only carries the object key), so HEAD requests previously
ignored If-None-Match/If-Match/If-Modified-Since/If-Unmodified-Since
and always returned 200. Prod public buckets return 304/412 for HEAD
just like GET. Route HEAD through bucket.get() with onlyIf and discard
the body.

Conditional tests are now parameterized over both methods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tahmid-23 tahmid-23 changed the title [wrangler][miniflare] Serve local R2 bucket objects publicly via /cdn-cgi/mf/r2 [wrangler][miniflare] Serve local R2 bucket objects publicly via /cdn-cgi/local/r2/public Jun 9, 2026
@NuroDev

NuroDev commented Jun 9, 2026

Copy link
Copy Markdown
Member

Do you think I should add a CORS response that allows all origins, or would that be too broad?

Good point @tahmid-23! Yes let's add some CORS headers to allow all origins 👍

tahmid-23 and others added 2 commits June 9, 2026 15:45
The new r2:public service previously copied the 2023-07-24 date from the
neighboring bucket object worker for parity, but new embedded workers
should use a recent compatibility date (matching the local explorer
worker).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Serves cross-origin requests from frontend dev servers running on other
localhost ports (the entry worker already rejects non-localhost origins
for /cdn-cgi/* paths before they reach this worker). Preflight OPTIONS
requests are answered with 204, and all response headers are exposed so
clients can read ETag, Content-Range, etc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tahmid-23

Copy link
Copy Markdown
Contributor Author

Do you think I should add a CORS response that allows all origins, or would that be too broad?

Good point @tahmid-23! Yes let's add some CORS headers to allow all origins 👍

done @NuroDev

The local helper existed only to thread experimentalLocalPublic through
the bucket entries. Now that the flag is gone, it is identical to the
shared namespaceEntries helper, so use that again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@workers-devprod

workers-devprod commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/miniflare/src/workers/r2/public.worker.ts Outdated
Comment thread packages/miniflare/src/plugins/r2/index.ts
devin-ai-integration[bot]

This comment was marked as resolved.

…blic routes

Previously a failed conditional GET/HEAD returned 412 whenever the
request carried an If-Match or If-Unmodified-Since header, even when
that header's condition passed and the failure actually came from
If-None-Match or If-Modified-Since — which RFC 9110 §13.2.2 defines as
a 304.

`bucket.get()` reports a failed conditional only as a body-less result,
without naming the failed header. On failure, the worker now re-runs
the lookup with just the If-Match/If-Unmodified-Since pair: if those
preconditions fail on their own the response is 412, otherwise the
failure came from a cache validator and the response is 304. Reusing
the bucket's conditional evaluation keeps etag parsing and date
granularity consistent rather than reimplementing them in the worker,
and successful requests still cost a single `get()`.

Adds tests covering the RFC 9110 §13.2.2 decision table, including
both within-family skip rules and precondition precedence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tahmid-23 tahmid-23 force-pushed the r2-local-public-bucket branch from 199dd3d to 59d440f Compare June 10, 2026 06:23

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/miniflare/src/workers/r2/public.worker.ts

@NuroDev NuroDev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM! Thanks @tahmid-23!
Giving CI a run now to make sure all tests, formatting, linting, etc looks good

@workers-devprod workers-devprod left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeowners reviews satisfied

@github-project-automation github-project-automation Bot moved this from Untriaged to Approved in workers-sdk Jun 10, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 10, 2026

Copy link
Copy Markdown
create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@14119

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@14119

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@14119

miniflare

npm i https://pkg.pr.new/miniflare@14119

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@14119

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@14119

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@14119

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@14119

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@14119

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@14119

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@14119

wrangler

npm i https://pkg.pr.new/wrangler@14119

@cloudflare/wrangler-bundler

npm i https://pkg.pr.new/@cloudflare/wrangler-bundler@14119

commit: 59d440f

@NuroDev NuroDev merged commit 2047a32 into cloudflare:main Jun 10, 2026
63 checks passed
@github-project-automation github-project-automation Bot moved this from Approved to Done in workers-sdk Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants