# Presigned R2 Upload (/docs/experiments/presigned-r2-upload)



Generate **presigned PUT URLs** so browsers upload files directly to **R2** without routing file bytes through the Worker. Includes a demo HTML form at `GET /`.

## Features [#features]

* **POST /presign** - Returns a time-limited upload URL
* **Direct browser upload** - Client PUTs to R2 using the signed URL
* **Content-type validation** - Allowed types enforced at presign time
* **Demo page** - `GET /` with file picker and upload flow

## API Reference [#api-reference]

### POST /presign [#post-presign]

Generate a presigned PUT URL for R2.

<TypeTable
  type="{
  filename: {
    description:
      &#x22;Filename (alphanumeric, dot, dash, underscore; max 128 chars, no path segments).&#x22;,
    type: &#x22;string&#x22;,
    required: true,
  },
  contentType: {
    description:
      &#x22;MIME type. Allowed: image/png, image/jpeg, image/webp, text/plain, application/pdf.&#x22;,
    type: &#x22;string&#x22;,
    required: true,
  },
}"
/>

#### Example Request [#example-request]

```bash
curl -X POST "https://your-worker.workers.dev/presign" \
  -H "Content-Type: application/json" \
  -d '{"filename":"photo.png","contentType":"image/png"}'
```

#### Success Response [#success-response]

```json
{
  "uploadUrl": "https://account.r2.cloudflarestorage.com/bucket/uploads/uuid-photo.png?X-Amz-...",
  "key": "uploads/550e8400-e29b-41d4-a716-446655440000-photo.png",
  "contentType": "image/png",
  "maxBytes": 5242880,
  "expiresInSeconds": 900
}
```

Upload the file with a matching `Content-Type`:

```bash
curl -X PUT "$uploadUrl" -H "Content-Type: image/png" --data-binary @photo.png
```

#### Error Codes [#error-codes]

* `400` - Invalid JSON (`INVALID_BODY`), filename (`INVALID_FILENAME`), or content type (`INVALID_CONTENT_TYPE`)
* `502` - Presign failed, often missing R2 credentials (`PRESIGN_ERROR`)

### GET / [#get-]

Returns an HTML demo page with presign + direct PUT upload flow.

## Use Cases [#use-cases]

* Offload large file uploads from Workers to R2
* Learn aws4fetch presigned URL generation at the edge
* Prototype user-generated content uploads with size/type constraints

## Limitations [#limitations]

* Requires R2 API credentials as Worker secrets (`R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`)
* Browser uploads need R2 CORS configured for your origin
* Signed `Content-Type` must match the client PUT header exactly
* Documented max size 5 MB (enforced at presign documentation level)

## Deployment [#deployment]

<Steps>
  <Step>
    ### Click the deploy button [#click-the-deploy-button]

    [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/shrinathsnayak/cloudflare-experiments/tree/main/apps/experiments/presigned-r2-upload)
  </Step>

  <Step>
    ### Configure R2 and secrets [#configure-r2-and-secrets]

    ```bash
    wrangler secret put R2_ACCESS_KEY_ID
    wrangler secret put R2_SECRET_ACCESS_KEY
    ```

    Set `R2_ACCOUNT_ID` in `wrangler.json` vars. Configure R2 CORS for browser PUT uploads.
  </Step>

  <Step>
    ### Test presign [#test-presign]

    ```bash
    curl -X POST "https://your-worker.workers.dev/presign" \
      -H "Content-Type: application/json" \
      -d '{"filename":"test.txt","contentType":"text/plain"}'
    ```
  </Step>
</Steps>

## Local Development [#local-development]

```bash
cd apps/experiments/presigned-r2-upload
npm install
npm run dev
```

Open `http://localhost:8787/` for the demo upload form.

## Configuration [#configuration]

| Setting                | Purpose                          |
| ---------------------- | -------------------------------- |
| `UPLOADS`              | R2 bucket binding                |
| `R2_ACCOUNT_ID`        | Account ID for signing (var)     |
| `R2_ACCESS_KEY_ID`     | R2 API token access key (secret) |
| `R2_SECRET_ACCESS_KEY` | R2 API token secret (secret)     |

## Cloudflare Features Used [#cloudflare-features-used]

* **[Workers](https://developers.cloudflare.com/workers/)** - Edge compute runtime
* **[R2](https://developers.cloudflare.com/r2/)** - Object storage with presigned uploads
