Skip to content

[Bug]: breaking path prefixes like public/ → public%2F in s3 file service #14756

@mcqua007

Description

@mcqua007

Package.json file

{
  "name": "medusa project",
  "version": "0.0.1",
  "description": "The backend for the storefront",
  "author": "Medusa (https://medusajs.com)",
  "license": "UNLICENSED",
  "keywords": [
    "sqlite",
    "postgres",
    "typescript",
    "ecommerce",
    "headless",
    "medusa"
  ],
  "scripts": {
    "build": "medusa build",
  },
  "lint-staged": {
    "**/*": "prettier --write --ignore-unknown"
  },
  "dependencies": {
    "@clerk/backend": "^2.29.3",
    "@clerk/express": "^1.7.63",
    "@clerk/shared": "^3.43.0",
    "@medusajs/admin-sdk": "2.13.1",
    "@medusajs/admin-shared": "2.13.1",
    "@medusajs/cache-redis": "2.13.1",
    "@medusajs/cli": "2.13.1",
    "@medusajs/dashboard": "2.13.1",
    "@medusajs/draft-order": "2.13.1",
    "@medusajs/event-bus-redis": "2.13.1",
    "@medusajs/file-s3": "2.13.1",
    "@medusajs/framework": "2.13.1",
    "@medusajs/js-sdk": "2.13.1",
    "@medusajs/medusa": "2.13.1",
    "@medusajs/types": "2.13.1",
    "@medusajs/ui": "^4.0.21",
    "@medusajs/utils": "2.13.1",
    "@medusajs/workflow-engine-redis": "2.13.1",
    "@mikro-orm/core": "6.4.16",
    "@mikro-orm/knex": "6.4.16",
    "@mikro-orm/migrations": "6.4.16",
    "@mikro-orm/postgresql": "6.4.16",
    "@opentelemetry/api": "^1.9.0",
    "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
    "@opentelemetry/instrumentation-pg": "^0.56.1",
    "@opentelemetry/resources": "^2.0.1",
    "@opentelemetry/sdk-node": "^0.203.0",
    "@opentelemetry/sdk-trace-node": "^2.0.1",
    "@react-email/components": "0.5.7",
    "@segment/analytics-node": "^2.3.0",
    "@sentry/node": "^10.9.0",
    "@sentry/opentelemetry-node": "^7.114.0",
    "@tanstack/react-query": "^5.67.3",
    "awilix": "^8.0.1",
    "dotenv": "^16.5.0",
    "express": "^4.21.0",
    "jsonwebtoken": "^9.0.2",
    "mime-types": "^2.1.35",
    "multer": "^2.0.2",
    "nodemailer": "^7.0.4",
    "pg": "^8.13.0",
    "posthog-node": "^4.17.1",
    "rate-limiter-flexible": "^7.1.1",
    "resend": "^4.6.0",
    "sharp": "^0.34.3",
    "slugify": "^1.6.6",
    "ssh2-sftp-client": "^12.0.0",
    "stripe": "^17.7.0",
    "svix": "^1.68.0",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@clerk/types": "^4.101.11",
    "@faker-js/faker": "^9.2.0",
    "@medusajs/test-utils": "2.13.1",
    "@mikro-orm/cli": "6.4.16",
    "@swc/core": "1.5.7",
    "@swc/jest": "^0.2.36",
    "@types/jest": "^29.5.13",
    "@types/jsonwebtoken": "^9.0.7",
    "@types/multer": "^2.0.0",
    "@types/node": "^20.0.0",
    "@types/nodemailer": "^6.4.17",
    "@types/pg": "^8.11.10",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.3.2",
    "@types/ssh2-sftp-client": "^9.0.4",
    "@vitejs/plugin-react": "^4.4.1",
    "jest": "^29.7.0",
    "msw": "2.6.8",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-email": "4.3.2",
    "rimraf": "^6.0.1",
    "ts-node": "^10.9.2",
    "tsc-alias": "^1.8.16",
    "tsx": "^4.19.3",
    "type-fest": "^4.37.0",
    "typescript": "^5.8.3",
    "vite": "^5.2.11",
    "vite-tsconfig-paths": "^5.1.4",
    "vitest": "^3.1.2"
  },
  "engines": {
    "node": ">=22.0.0 <23.0.0",
    "pnpm": ">=10.6"
  },
  "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
}

Node.js version

v22

Database and its version

16.2

Operating system name and version

MacOS Tahoe 26.2 (25C56)

Browser name

No response

What happended?

This bug was introduced in PR #14209 - "fix: escape non-ascii characters in filenames in s3 file provider".

The PR added encodeURIComponent(fileKey) to handle non-ASCII characters in filenames (e.g., Chinese characters). The problem is encodeURIComponent also encodes /%2F, which breaks path prefixes like public/public%2F.

The problem statement is valid - non-ASCII filenames (e.g., 文档.pdf) and special characters (file?.jpg) do need URL encoding for S3 URLs to work. But the PR uses encodeURIComponent(fileKey) on the entire key including the path prefix. This fixes the edge case (non-ASCII characters) but broke the common case (any file with a path prefix containing /.

// fileKey = "public/thumbnail.jpg"
url: `${this.config_.fileUrl}/${encodeURIComponent(fileKey)}`
// Result: https://cdn.example.com/public%2Fthumbnail.jpg  ❌

The fix should have used encodeURI() instead (which preserves /), or only encoded the filename portion, not the entire key including the path prefix.

url: `${this.config_.fileUrl}/${encodeURI(fileKey)}`
// Result: https://cdn.example.com/public/文档.pdf  ✓

Or

url: `${this.config_.fileUrl}/${prefix}${encodeURIComponent(filename)}`
// Result: https://cdn.example.com/public/文档.pdf  && https://cdn.example.com/public/image.png   ✓

Steps to Reproduce

  1. Configure S3 file provider with a prefix (e.g., prefix: "public/")
  2. Upload any file
  3. Observe the returned URL

Expected behavior

fileKey: "public/thumbnail-01ABC123.jpg"
URL: "https://cdn.example.com/public/thumbnail-01ABC123.jpg"

Actual behavior

fileKey: "public/thumbnail-01ABC123.jpg"
URL: "https://cdn.example.com/public%2Fthumbnail-01ABC123.jpg"

Link to reproduction repo

n/a

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions