Everything reported healthy. The bucket, the files, the custom domain, the DNS record. And every request to media.example.com returned a 404.
It cost me most of a day, so here it is in full.
The symptom
I had 60 video files in an R2 bucket, served through a custom domain — media.example.com. Every request returned 404. Not a Cloudflare error page, not an XML error from the bucket. A 404.
The obvious suspects were all clean:
Files present in the bucket, correct sizes, correct content types
Custom domain listed as Active in the R2 settings
DNS record present and proxied
Object keys matched the paths I was requesting, case included
No bucket-level access rules in the way
I re-uploaded the files. I recreated the custom domain. I checked object keys character by character. All of it was wasted, because none of it was broken.
The tell
The 404 page had my own styling on it.
media.example.com had nothing to do with my website. It was a bucket of .mp4 files. There was no reason for it to serve HTML at all, let alone HTML with my fonts and my copy in it.
That's the diagnostic. If a subdomain that should be serving binary files returns your website's own error page, something else on your account is answering the request before R2 ever sees it.
The cause
I had a Worker serving the main site, with a route pattern of:
.example.com/
I set that up months earlier and stopped thinking about it.
Worker routes take precedence over R2 custom domains on the same hostname. So the request to media.example.com/clip-01.mp4 matched the wildcard, hit the Worker, the Worker looked for a static asset at /clip-01.mp4, found nothing, and served the site's 404 page.
R2 was never reached. Which is why the R2 dashboard showed everything working — it was working. It just wasn't receiving anything.
That's the part that makes this expensive to debug. Cloudflare reports the health of each product independently, and both were telling the truth. R2 was configured correctly. The Worker was running correctly. Neither dashboard knows the other one exists.
Confirming it in thirty seconds
curl -sI https://media.example.com/some-file.mp4
Look at content-type. If it says text/html on a request for a video file, you are not talking to R2.
Then go to Workers & Pages → your Worker → Settings → Domains & Routes and read the patterns. Any wildcard covering your subdomain is your problem.
The fix
Stop using a wildcard route. Enumerate the hostnames the Worker should actually serve:
example.com/*
www.example.com/*
That was the whole fix. The videos worked immediately, no changes to R2 at all.
A more specific route also takes precedence over a less specific one, so in principle you can carve out an exclusion rather than replacing the wildcard. I didn't go that way — I couldn't see a reason to keep a wildcard I'd only ever wanted for two hostnames — so I can't tell you how cleanly that behaves in practice. If you have a genuine reason to keep the wildcard, test the exclusion before trusting it.
Why this is easy to hit
The ordering does it. You set up the Worker first, when the account has one thing on it and a wildcard route looks like sensible future-proofing. You add R2 months later, on a hostname you never thought about when you wrote that pattern.
Nothing warns you. Adding a custom domain to a bucket doesn't check for conflicting Worker routes, and the Worker doesn't know it just started answering for a hostname it was never meant to serve.
So: when you add any new subdomain to a Cloudflare account that already runs a Worker, read your Worker routes first. The same trap applies to Pages custom domains and anything else bound to a hostname under a wildcard you forgot about.
I hit this building https://edmundwarde.com/, a static Astro site on Workers static assets. There's a second, smaller Workers routing trap in the same family — redirects written in Worker code that never execute — which I've written up separately.
Top comments (0)