R2 keys with multiple slashes in key require url encoding or it 404s

Does publicly accessing an R2 object with slashes in its name 404 unless you url encode the slashes?

I’ve got an object uploaded under the key “0cf9bed757176aabf247d9fd34f3e57a4cea05ec/_next/static/chunks/1046.d7427b02ac0637ec.js” and the domain “https://static.weights.gg/0cf9bed757176aabf247d9fd34f3e57a4cea05ec/_next/static/chunks/webpack-ae223e714b298506.js” - however, the url 404s when I use actual slashes (ie navigate to that url). However, if I use the one with %2F instead of slash, it works.


This isn’t really expected, is it? Was it a mistake I made when uploading the asset? Or is this a known issue?

The key looks correct in the cloudflare dashboard

1 Like

I’m facing this exact same issue.

The code I used to generate this was:

import "dotenv/config";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { fileURLToPath } from "url";
import path, { dirname } from "path";
import { promises as fs, createReadStream } from "fs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const s3 = new S3Client({
  region: "auto",
  endpoint: `https://${process.env.PROXY_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.PROXY_R2_ACCESS_KEY_ID || "",
    secretAccessKey: process.env.PROXY_R2_SECRET_ACCESS_KEY || "",
const releaseversion = process.env.RELEASE_VERSION || process.argv.pop();

// read everything in .next/static and upload to /_next/static
const staticDir = `${__dirname}/../.next/static`;

async function uploadDir(s3Path: string, bucketName: string) {
  async function getFiles(dir: string): Promise<string | string[]> {
    const dirents = await fs.readdir(dir, { withFileTypes: true });
    const files = await Promise.all(
      dirents.map((dirent) => {
        const res = path.resolve(dir, dirent.name);
        return dirent.isDirectory() ? getFiles(res) : res;
    return Array.prototype.concat(...files);

  const files = (await getFiles(s3Path)) as string[];
  console.log(`Uploading ${files.length} files to ${bucketName}`);
  const uploads = files.map((filePath) => {
    const p = [releaseversion || "", "_next", "static", path.relative(s3Path, filePath)];
    return s3.send(
      new PutObjectCommand({
        Bucket: bucketName,
        Key: p.filter(Boolean).join("/"),
        Body: createReadStream(filePath),
        Metadata: {
          build: releaseversion || "unknown",
  await Promise.all(uploads);
  console.log("Upload complete");

await uploadDir(staticDir, "web-static-assets");

Hey there,

Thank you for reporting this issue with R2. We are currently investigating the issue and will provide additional updates through the ticket you have submitted with us.

It has been 3 months since this problem happen , still nothing ?

Adding here that were experiencing the same issue.

We’ve had files that were uploaded almost a year ago and were working that now this month have stopped. Spent a lot of time troubleshooting but ultimately have come to the conclusion that the R2 dashboard is adding incorrect slash operators to the url presented. Our files rely on a relative path to load a webpage like folder structure for some training files. With the current operators being presented we have to replace the 2%F with the correct / or else our files do not route to the correct relative path.