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
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
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
S3Clientdanssrc/lib/r2.tsutiliseendpoint: https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.cometregion: "auto". - L’expiration de l’URL présignée est courte (60–300 secondes) ; pas une journée entière.
- Le
PUTdu navigateur inclut l’en-têteContent-Typecorrespondant à celui utilisé lors de la présignation — les discordances provoquent une erreur 403. NEXT_PUBLIC_R2_PUBLIC_URLpointe vers le domaine public du bucket, pas vers le point de terminaison de l’API R2.FileUpload.tsxcommence 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
# Start dev serverbun dev
# Test presign endpoint directlycurl -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-endcurl -X PUT "<uploadUrl>" \ -H "Content-Type: image/png" \ --data-binary @test.png -v
# Fetch the public URL to verify the file is readablecurl -I "<publicUrl>"6. Échecs courants
- 403 sur PUT — L’en-tête
Content-Typedans 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érifiezR2_BUCKET_NAMEdans 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
PUTdepuis 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 degetSignedUrlpour PUT — flux différent, code client différent requis.
7. Invite de correction
The browser PUT to R2 returns 403 SignatureDoesNotMatch.
The Content-Type passed to getSignedUrl must exactly match the Content-Typeheader sent by the browser. Update the presign route to pass the contentTypefrom the request body into getSignedUrl, and update FileUpload.tsx to setthe Content-Type header on the PUT request to the same value.
Also confirm the S3Client endpoint is: https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.comnot the generic AWS S3 endpoint.8. Description de la PR
## 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 PUTfrom your app origin.