Volley / Templates / S3 presigned URL upload

Storage template

S3 presigned URL upload sequence diagram

The browser uploads the file straight to S3, so large files never pass through your server. The diagram shows the signed URL's query parameters, the PUT, the ETag S3 returns and the event it sends afterwards.

Sequence diagram of a direct browser upload to S3 across Browser, App Server, S3 and Queue: request an upload URL, sign it locally, PUT the file to S3 with a 200 and ETag or a 403, confirm with the app server, a HEAD check, and an optional ObjectCreated event to a queue.

Click any arrow in the editor to see its request and response as a table or the raw body. Change the hosts, ids and fields to match your integration, then share a link or embed it in your README.

Step by step

  1. The browser asks your server for an upload URL with the file name, content type and size.
  2. Your server checks the request and signs a URL. Signing is a local SigV4 calculation with no call to AWS. The URL carries X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders and X-Amz-Signature.
  3. The browser PUTs the file to S3 with the same Content-Type it signed. S3 answers 200 with an ETag, or 403 if the signature does not match or has expired.
  4. The browser tells your server it is done, and your server checks the object with a HEAD request.
  5. S3 can also send an event, ObjectCreated:Put, to a queue for scanning or thumbnails.

Easy to get wrong

  • The bucket needs a CORS rule that allows PUT from your origin and exposes ETag, or the browser cannot read it.
  • A presigned PUT does not limit the size unless you sign Content-Length. Check the object afterwards, or use a presigned POST with a content-length-range condition.
  • Keep expiry short. Anyone with the URL can upload until X-Amz-Expires runs out.

The source

The whole diagram is this plain text. Copy it into your repo, or open it in the editor.

s3-presigned-upload.volley
title: Direct browser upload to S3 with a presigned URL
order: Browser, App Server, S3, Queue

Browser -> App Server: POST /uploads
in: File details
```json
{ "filename": "q3-report.pdf", "contentType": "application/pdf", "size": 2483017 }
```

App Server -> App Server: Check the type, size and user, then sign a PUT URL
note: Signing is local. The server computes a SigV4 signature with its AWS credentials and sends nothing to AWS. Anyone with the URL can use it until it expires.

App Server --> Browser: 200 OK
out: Presigned URL (query parameters one per line)
  uploadId: upl_9c41
  url: https://example-uploads.s3.us-east-1.amazonaws.com/uploads/usr_42/9c41e2-q3-report.pdf
  X-Amz-Algorithm: AWS4-HMAC-SHA256
  X-Amz-Credential: AKIAIOSFODNN7EXAMPLE/20261019/us-east-1/s3/aws4_request
  X-Amz-Date: 20261019T140000Z
  X-Amz-Expires: 300
  X-Amz-SignedHeaders: content-type;host
  X-Amz-Signature: <redacted>

=: {fab-aws} Straight to the bucket

Browser -> S3: PUT /uploads/usr_42/9c41e2-q3-report.pdf
in: Upload
```http
PUT /uploads/usr_42/9c41e2-q3-report.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20261019%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261019T140000Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=<redacted> HTTP/1.1
Host: example-uploads.s3.us-east-1.amazonaws.com
Content-Type: application/pdf
Content-Length: 2483017

(2,483,017 bytes of PDF)
```

if: Signature is valid and not expired
  S3 --> Browser: 200 OK
  out: Response headers
    ETag: "9b2cf535f27731c974343645a3985328"
    x-amz-server-side-encryption: AES256
else: Expired, or a header differs from what was signed
  S3 --> Browser: 403 Forbidden
  out: Error
  ```text
  <Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided.</Message></Error>
  ```
end
note: The `Content-Type` sent must match the one signed. The bucket needs a CORS rule that allows `PUT` from your origin and exposes `ETag`.

Browser -> App Server: POST /uploads/upl_9c41/complete
in: Completion
```json
{ "etag": "\"9b2cf535f27731c974343645a3985328\"" }
```

App Server -> S3: HEAD /uploads/usr_42/9c41e2-q3-report.pdf
S3 --> App Server: 200 OK
out: Object headers
  Content-Length: 2483017
  Content-Type: application/pdf
  ETag: "9b2cf535f27731c974343645a3985328"
note: Check the object really exists and matches the size you approved before you trust it.

App Server --> Browser: 200 OK
out: File
```json
{ "id": "file_31", "name": "q3-report.pdf", "status": "ready" }
```

group: Optional S3 event notification
  S3 ->> Queue: s3:ObjectCreated:Put
  in: Event
  ```json
  {
    "Records": [
      {
        "eventVersion": "2.1",
        "eventSource": "aws:s3",
        "awsRegion": "us-east-1",
        "eventTime": "2026-10-19T14:00:41.512Z",
        "eventName": "ObjectCreated:Put",
        "s3": {
          "bucket": { "name": "example-uploads", "arn": "arn:aws:s3:::example-uploads" },
          "object": { "key": "uploads/usr_42/9c41e2-q3-report.pdf", "size": 2483017, "eTag": "9b2cf535f27731c974343645a3985328", "sequencer": "0067139A49B2C7D1E5" }
        }
      }
    ]
  }
  ```
end
note: The event arrives whether or not the browser reports back, so it is the safer trigger for scanning or thumbnails. Object keys in events are URL encoded.

Related

Make this S3 presigned URL upload diagram yours.

Open it in the editor, change it to match your integration and share the link. Free, with no account.