User appka požádá storage-svc o presigned GET URL pro existující objekt v R2 bucketu. Storage-svc validuje request (HMAC token, key sanity, object existuje) a vrátí presigned URL s 60min TTL. Klient pak stahuje přímo z Cloudflare R2.
- Delší TTL (60 min vs 15 min upload) — uživatelé občas otevřou link a stáhnou ho později.
- Storage-svc NEvolá S3 SDK
HeadObjectpřed presignem (extra HTTP roundtrip) — pokud klient zavolá GET a objekt neexistuje, R2 vrátí 404. Storage-svc tedy nemůže vědět dopředu, jestli objekt existuje, ale to je OK (cheaper, R2 sama vrátí 404 klientovi).
sequenceDiagram
actor EndUser as User Browser
participant UA as User App BE
participant SS as storage-svc
participant CF as Cloudflare R2
EndUser->>+UA: action that needs file download
UA->>+SS: POST /api/v1/storage/presign-download <br> X-Talkide-App-Token: {hmac} <br> { key }
SS->>SS: verify HMAC token (constant-time compare)
alt token invalid
SS-->>UA: 401 Unauthorized <br> ErrorResponse
end
SS->>SS: validate + sanitize key
alt invalid
SS-->>UA: 400 Bad Request <br> ErrorResponse
end
SS->>+CF: AWS SigV4 sign — presigned GET URL <br> Expires=60min
CF->>-SS: signed URL
SS->>-UA: 200 OK <br> { downloadUrl, key, expiresAt }
UA->>-EndUser: relay downloadUrl
EndUser->>+CF: GET {downloadUrl}
alt object exists
CF-->>EndUser: 200 OK <br> file bytes
else object missing
CF-->>EndUser: 404 Not Found <br> R2 native XML error
end
deactivate CF
API Contract
POST /api/v1/storage/presign-download PresignDownloadRequest:
{
"key": "uploads/avatars/user-123.png"
}
Headers:
X-Talkide-App-Token: <32B hex HMAC token>— required.Content-Type: application/json
200 OK PresignDownloadResponse:
{
"data": {
"downloadUrl": "https://{accountId}.r2.cloudflarestorage.com/talkide-app-popelkam-todo-list/uploads/avatars/user-123.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Date=...&X-Amz-Expires=3600&X-Amz-Signature=...",
"key": "uploads/avatars/user-123.png",
"expiresAt": "2026-05-24T11:30:00Z"
}
}
Error responses
400 Bad Request (invalid key) ErrorResponse:
{
"status": 400,
"code": "STORAGE_INVALID_KEY",
"message": "Key contains forbidden characters or path traversal"
}
400 Bad Request (validation) ErrorResponse:
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{ "field": "key", "message": "must not be blank" }
]
}
401 Unauthorized ErrorResponse:
{
"status": 401,
"code": "AUTHENTICATION_FAILED",
"message": "Invalid or missing storage token"
}
500 Internal Server Error ErrorResponse:
{
"status": 500,
"code": "STORAGE_PRESIGN_FAILED",
"message": "Failed to generate presigned URL"
}
Pozn.: žádný 404 endpoint-side — storage-svc nedělá HEAD před presignem. Klient dostane 404 od R2 přímo, pokud objekt neexistuje.
Optional: forced download (Content-Disposition)
User-app může chtít, aby browser link otevřel jako download místo inline preview. Lze override přes parametry presignu:
{
"key": "exports/report.pdf",
"responseContentDisposition": "attachment; filename=\"report.pdf\""
}
Storage-svc předá response-content-disposition do AWS SigV4 podpisu (R2 honoruje, je to standardní S3 query param). MVP: optional field, default nepřítomný (browser sám rozhodne podle content-type).
responseContentDisposition size limit: 1–512 znaků, nesmí obsahovat CRLF (header injection prevention).
Frontend
User-app FE volání jde přes BE proxy (BE drží HMAC token, FE nesmí). Žádný přímý FE → storage-svc call.
Validations (user-app SDK)
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| key | not_blank, sanitized | 1 – 1024 chars | viz UC-12002 sanitizeKey | |
| responseContentDisposition | optional, no CRLF | 1 – 512 chars | — |
Backend (storage-svc)
Validations
| Field | Constraints | Size | Pattern | Note |
|---|---|---|---|---|
| X-Talkide-App-Token (header) | not_blank, constant-time compare | 64 hex chars | ^[0-9a-f]{64}$ | |
| key | not_blank, sanitized | 1 – 1024 chars | viz UC-12002 | |
| responseContentDisposition | optional, no CRLF | 1 – 512 chars | ^[^\r\n]+$ |
Test Cases
| GIVEN | WHEN | THEN |
|---|---|---|
| valid HMAC token, valid key, objekt existuje v R2 | POST /presign-download je volán | 200 OK; vrácen downloadUrl s expiresAt ~60 min v budoucnu |
| valid HMAC token, valid key, objekt NEexistuje v R2 | POST /presign-download je volán | 200 OK; vrácen presigned URL (storage-svc neví že objekt chybí). Klient pak dostane 404 od R2 při GET |
| chybí HMAC token | POST /presign-download je volán | 401 AUTHENTICATION_FAILED |
| HMAC token nesprávný | POST /presign-download je volán | 401 AUTHENTICATION_FAILED |
key obsahuje .. | POST /presign-download je volán | 400 STORAGE_INVALID_KEY |
| key prázdný | POST /presign-download je volán | 400 VALIDATION_ERROR |
valid request s responseContentDisposition attachment; filename="x.pdf" | POST /presign-download je volán | 200 OK; presigned URL obsahuje response-content-disposition query param |
| responseContentDisposition obsahuje CRLF | POST /presign-download je volán | 400 VALIDATION_ERROR (header injection prevention) |
| AWS SDK presign volání selže | POST /presign-download je volán | 500 STORAGE_PRESIGN_FAILED |
| klient GET na URL za 30 min od presignu | klient stahuje | 200 OK + bytes (URL platná) |
| klient GET na URL za 61 min od presignu | klient stahuje | 403 Forbidden (URL expired) |
| klient GET na URL s manipulovaným query param | klient stahuje | 403 Forbidden (signature mismatch) |
| valid presign, ale dva paralelní GET na stejné URL | klient stahuje 2× | obě 200 OK + bytes (R2 nedrží stav podpisu, jen verifikuje) |
Caching guidance (pro user-app SDK uživatele)
Storage-svc je stateless vůči presignu — každé volání generuje nový URL. User-app SDK může cachovat výsledek (např. 30 min v Redis) pokud klient potřebuje opakovaně stejný link. Doporučení v UC-12005 SDK docs.
Nedělejte to pro public/exposed flow (např. hot-link share), kde TTL = security control. Cache pouze pro server-side internal use.
Out of scope (this UC)
- Range requests (HTTP
Range: bytes=...) — R2 podporuje nativně, storage-svc se neúčastní (klient ↔ R2 přímo). - Conditional GET (
If-Modified-Since,ETag) — analogicky R2 native. - Streaming přes storage-svc (BE proxy data) — záměrně NE (data path je direct).
- Pre-flight HEAD endpoint na storage-svc — záměrně NE (R2 sama vrátí 404).
Thanks for the feedback.