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.
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
- The browser asks your server for an upload URL with the file name, content type and size.
- 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-SignedHeadersandX-Amz-Signature. - The browser PUTs the file to S3 with the same
Content-Typeit signed. S3 answers 200 with anETag, or 403 if the signature does not match or has expired. - The browser tells your server it is done, and your server checks the object with a
HEADrequest. - 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
PUTfrom your origin and exposesETag, 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 acontent-length-rangecondition. - Keep expiry short. Anyone with the URL can upload until
X-Amz-Expiresruns 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.