P PasteCode
Playbook

Prompt-to-PR: Add Cloudflare R2 File Upload

SOP de bout en bout pour intégrer les téléchargements présignés R2 dans une application Next.js ou Cloudflare Workers — liaison de bucket, URLs présignées et flux de téléchargement client.

CursorClaude CodeCodexWindsurf Next.jsCloudflareTypeScript
.md .json Difficulté: Moyen Mis à jour 8 juin 2026

Ajoutez un téléchargement de fichier vers R2 sans passer les données binaires via votre serveur. L’agent génère une URL présignée côté serveur ; le PUT du navigateur va directement à R2.

1. Exigences

Les utilisateurs peuvent télécharger des fichiers (images, PDF, jusqu’à 10 Mo) via une interface glisser-déposer. Le serveur émet une URL PUT présignée ; le navigateur envoie le fichier directement à Cloudflare R2. Une URL de lecture publique est renvoyée après le téléchargement. Aucun fichier ne transite par le serveur Next.js.

2. Première invite

First Prompt
Add Cloudflare R2 file upload to this Next.js 15 App Router project.
Requirements:
- Use presigned PUT URLs (not proxy upload). The Next.js route only issues
the presigned URL; the browser uploads directly to R2.
- Install `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
(R2 is S3-compatible).
- Create `src/app/api/upload/presign/route.ts` (POST). Accept JSON body
`{ filename: string; contentType: string; size: number }`. Validate:
allowed MIME types (image/jpeg, image/png, image/webp, application/pdf),
max size 10 MB. Return `{ uploadUrl, publicUrl, key }`.
- Read R2 credentials from env vars:
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY,
R2_BUCKET_NAME, NEXT_PUBLIC_R2_PUBLIC_URL.
- Create `src/components/FileUpload.tsx` — a Client Component with a
drag-and-drop zone. On file select: POST to /api/upload/presign, then
PUT the file to the returned uploadUrl with the correct Content-Type header.
Show progress, handle errors, and call an `onUpload(publicUrl)` callback.
- Do not store the file in any database table; that is the caller's
responsibility via the onUpload callback.

3. Modifications de fichiers attendues

package.json (@aws-sdk/client-s3, @aws-sdk/s3-request-presigner)
src/app/api/upload/presign/route.ts (new — presign endpoint)
src/components/FileUpload.tsx (new — drag-and-drop client component)
src/lib/r2.ts (new — S3Client singleton)
.env.local.example (R2_* vars)

4. Liste de vérification

  • Le point de terminaison de présignation valide le type MIME et la taille avant d’appeler R2 — les mauvais téléchargements sont rejetés avant tout appel AWS.
  • Le S3Client dans src/lib/r2.ts utilise endpoint: https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com et region: "auto".
  • L’expiration de l’URL présignée est courte (60–300 secondes) ; pas une journée entière.
  • Le PUT du navigateur inclut l’en-tête Content-Type correspondant à celui utilisé lors de la présignation — les discordances provoquent une erreur 403.
  • NEXT_PUBLIC_R2_PUBLIC_URL pointe vers le domaine public du bucket, pas vers le point de terminaison de l’API R2.
  • FileUpload.tsx commence par "use client" — pas d’imports serveur.
  • Il n’y a pas de vérification de taille ou de type uniquement côté client — le serveur doit également valider.
  • La clé utilise un préfixe aléatoire (par exemple crypto.randomUUID()) pour éviter les collisions de noms de fichiers.

5. Commandes de test

Terminal window
# Start dev server
bun dev
# Test presign endpoint directly
curl -X POST http://localhost:3000/api/upload/presign \
-H "Content-Type: application/json" \
-d '{"filename":"test.png","contentType":"image/png","size":12345}' | jq .
# Confirm returned uploadUrl is an R2 presigned URL (contains X-Amz-Signature)
# Then PUT a real file to confirm end-to-end
curl -X PUT "<uploadUrl>" \
-H "Content-Type: image/png" \
--data-binary @test.png -v
# Fetch the public URL to verify the file is readable
curl -I "<publicUrl>"

6. Échecs courants

  • 403 sur PUT — L’en-tête Content-Type dans le PUT du navigateur ne correspond pas à celui utilisé lors de la présignation. Assurez-vous que les deux utilisent exactement la même chaîne.
  • NoSuchBucket — mauvais nom de bucket ou ID de compte. Vérifiez R2_BUCKET_NAME dans le tableau de bord Cloudflare.
  • InvalidAccessKeyId — Le jeton API R2 nécessite l’autorisation “Object Read & Write”, pas seulement “Read”.
  • Erreur CORS sur PUT direct — La politique CORS du bucket R2 doit autoriser PUT depuis votre origine. Configurez-la dans le tableau de bord Cloudflare sous R2 → Settings → CORS.
  • L’agent utilise @aws-sdk/s3-presigned-post (pour POST) au lieu de getSignedUrl pour PUT — flux différent, code client différent requis.

7. Invite de correction

Fix Prompt
The browser PUT to R2 returns 403 SignatureDoesNotMatch.
The Content-Type passed to getSignedUrl must exactly match the Content-Type
header sent by the browser. Update the presign route to pass the contentType
from the request body into getSignedUrl, and update FileUpload.tsx to set
the Content-Type header on the PUT request to the same value.
Also confirm the S3Client endpoint is:
https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
not the generic AWS S3 endpoint.

8. Description de la PR

PR description
## Feature: Cloudflare R2 file upload via presigned URLs
- New POST `/api/upload/presign` validates MIME type and size, then returns
a short-lived R2 presigned PUT URL
- Files upload directly from the browser to R2 — zero binary data through
the Next.js server
- New `<FileUpload>` component: drag-and-drop, progress indicator, error state
- Random UUID key prefix prevents filename collisions
**Required env vars** (see `.env.local.example`):
`R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`,
`R2_BUCKET_NAME`, `NEXT_PUBLIC_R2_PUBLIC_URL`
**R2 bucket setup**: enable public access and add a CORS rule allowing PUT
from your app origin.