Downloading Files & Presigned URLs

Downloading Files & Presigned URLs

Serving downloads securely: server-provided paths, presigned URLs, and best-practice patterns

This guide explains practical workflows for letting users download stored files safely — whether you stream files through your server or give temporary direct access to your storage.

Start here: this page covers how to present download links for files you store, when to return a server-provided (proxied) download versus when to issue a presigned URL for direct storage access, how to generate links with safe headers and expirations, and patterns for very large files or limiting access. It assumes your storage (S3-compatible) is configured via environment variables (bucket name, region, credentials) and that file size/type rules are enforced at upload time.

Covered features

Serve download links

How to provide safe download links from the server and how clients should consume them.

Presigned URLs

When and how to use temporary presigned URLs so clients can download directly from storage without exposing credentials.

Best-practice patterns

Rules for small vs large files, header choices (filename, inline vs attachment), expiry, and CORS considerations.

Large file strategies

Recommendations for range requests, resumable downloads and when to proxy large streams via the server.

Security & limits

Env var requirements, TTL guidance, and how to avoid leaking long-lived access.

Client UX

How to present links in web and mobile apps to yield correct filename, show progress, and fall back safely.

Workflow A — Download using a server-provided path (proxied download)

Use this pattern when you want full access control and the server must validate every request (small to medium files, audit/logging, on-the-fly transformations).

Server-proxied download — step-by-step

1

Step 1: Identify the file to download

In your UI let the user choose the item to download (for example a document entry). Each item should be referenced by the stored path or ID recorded in the item’s metadata.

2

Step 2: Ask your backend for a download link/action

From the client, invoke your backend’s download action for that file. The backend should verify the requesting user’s authorization and any business rules (ownership, expiration, role checks).

3

Step 3: Server prepares or streams the file

If authorized, your server either (A) streams the file from storage and returns it directly to the client, or (B) returns a short, internal path that the client can call to start the proxied download.

  • Streaming keeps control entirely on the server.
  • Return appropriate headers (Content-Type, Content-Length if known) and Content-Disposition for filename and inline/attachment behavior.
4

Step 4: Client starts the download

For web:

  • Use a normal link or programmatically create a hidden link and set the href to the server download action (open in new tab or set the download attribute). For mobile or single-page apps:
  • Use a fetch-like request to stream and save to local storage or present a progress UI.
5

Step 5: Handle errors and timeouts gracefully

Show retry UI, handle slow connections, and differentiate authorization failures vs network errors. Keep logs on the server for failed attempts if you need auditing.

6

Step 6: Clean up and audit

Record successful downloads if required for compliance. If files are temporary, consider a short server cache or immediate eviction after delivery.

When to prefer server-proxied downloads

If you must run virus scans, apply transformations (image resizing, stamping), require strict access logging, or must keep S3 private at all times, proxying through your server is the simplest and most auditable approach.

Small files = low cost to proxy

Proxying is inexpensive and simple for files typically under a few MB. It simplifies CORS, preserves filenames, and makes authorization straightforward.

Workflow B — Generate presigned URLs (direct storage access)

Use presigned URLs when you want clients to download large files directly from S3-compatible storage without routing bytes through your server.

Presigned URL generation & use — step-by-step

1

Step 1: Client requests a presigned URL

When the user clicks download, the client asks your backend for a presigned URL for that file. The backend must check user permissions first.

2

Step 2: Backend creates the presigned URL

The server generates a presigned GET link with:

  • A short expiration time (TTL).
  • Response headers (Content-Disposition, Content-Type) encoded so the storage returns the right filename and behavior.
  • Optional constraints (single-use, limited IPs if supported by your storage provider). Return that URL to the client.
3

Step 3: Client downloads directly from storage

The client uses the presigned URL directly in the browser or app. Options:

  • Open in a new tab or anchor link (recommended for browser downloads).
  • Use a native download manager or fetch for progress/resume if building custom UI.
4

Step 4: Respect expiry and handle errors

Presigned URLs expire. If a client’s URL expires, have the client request a fresh one. Handle 403/404 responses as “not authorized” or “link expired”.

5

Step 5: Audit usage at the server level

Because download bytes bypass your server, make sure you log when you issue presigned URLs and use access logs on storage to monitor actual downloads if needed.

6

Step 6: Secure the lifecycle

Set short TTLs for sensitive files, and prefer single-use generation patterns (generate on-demand, do not reuse long-lived presigned links).

Avoid long-lived presigned links

Presigned links grant direct access to storage; long TTLs increase the window for leaks. For sensitive content, keep TTLs short (minutes to hours depending on risk) and re-check authorization before issuing a new URL.

Workflow C — Handling very large files and resumable downloads

Large files bring special needs: retries, partial downloads, and avoiding server bandwidth spikes.

Large-file download patterns — step-by-step

1

Step 1: Record file size at upload

When a file is uploaded, capture its size so you can make decisions later (proxy vs presign).

2

Step 2: Choose direct download with byte-range support

For very large files, prefer presigned URLs and client-side range requests or a download manager that supports resuming, since this avoids moving the entire payload through your server.

3

Step 3: Enable range requests and resumable logic

Ensure the storage supports byte-range GETs and that the client can request partial ranges. If your UX must support pause/resume, use range headers and store progress client-side.

4

Step 4: Use multipart or chunked uploads on upload time

If uploads are part of the flow, avoid single-shot huge uploads. Chunked uploads reduce the chance that a single failure forces a full retry and make the file available progressively.

5

Step 5: Consider a hybrid: server-signed redirect for large files

If you need server-side authorization but want direct transfer, consider issuing a short-lived presigned URL and returning a redirect to the client so the download starts immediately without the server streaming bytes.

6

Step 6: Monitor failures and retry strategies

Detect common failure modes (network drop, partial content responses) and show the user resume/retry options. Provide guidance such as “try a wired connection” for very large downloads on mobile.

Detect size early to choose the right path

Use file-size thresholds (for example: < 5MB proxy; 5–100MB evaluate; >100MB prefer presigned download) so your app picks the best transfer strategy automatically.

  • Use when you need full control, transformation, scanning, or precise audit logs.
  • Simpler CORS behavior and easy to deliver the exact filename.
  • More server bandwidth and potential latency.
  • Best for small-to-medium files, private/temporary needs.
  • Minimal server bandwidth, scales well for many downloads.
  • Requires careful TTL and CORS configuration on storage.
  • Best for large files or when throughput matters.
  • You lose byte-level audit unless storage access logs are used.

When to proxy (short checklist)

  • File is small and access must be logged.
  • You need to transform or scan the file before delivery.
  • You must hide storage details entirely from clients.
  • You rely on server-side authentication on each request.

When to presign (short checklist)

  • Files are large or many concurrent downloads expected.
  • You want to offload bandwidth and improve throughput.
  • The storage supports short-lived links and response headers.
  • Your app can tolerate the storage-level auditing model.

Quick decision guide

Proxy when you need strict control, transformation, or auditing per-request. Presign when you need scale and can safely issue short-lived direct-access links. Combine both: proxy small/private files and presign large/public downloads.

Good UX reduces confusion (wrong filename, no progress, broken links).

Presenting download links safely and clearly

1

Step 1: Make the action discoverable

Show a clear download button with file name, size, and type. If the file may be large, note expected download time.

2

Step 2: Start with an authorization check

Before fetching bytes, call your backend to validate access and to obtain either the proxied path or the presigned URL.

3

Step 3: Use an anchor for simplest downloads (web)

For browser downloads, setting an anchor href to the returned URL and using the download attribute (when supported) is the simplest way to ensure the correct filename.

4

Step 4: Show progress for fetch-based downloads

If you use a programmatic fetch to stream data, show a progress indicator and allow cancel/resume for large transfers.

5

Step 5: Provide fallback

If a presigned URL fails (expired or blocked), present a friendly error and a button to retry obtaining a new link.

6

Step 6: Mobile considerations

Mobile apps should leverage native download managers or built-in viewers. Opening large downloads in the background and notifying the user when complete prevents blocking the app UI.

CORS & browser limitations

When using presigned URLs from a browser, ensure the storage bucket’s CORS settings allow requests from your site and that the presigned link includes or enforces the correct response headers. Otherwise the file may fail to download in some browsers.

Operational & security notes (gotchas)

Environment and limits — don’t forget these

  • Your storage bucket name, region, and credentials must be configured securely via environment variables or your secrets system.
  • Upload handlers commonly impose file size and type limits; very large files may be rejected unless chunking is used.
  • Never embed long-lived storage credentials in the client; use short-lived presigned URLs or server-side checks.

Logging and observability

Log the issuance of presigned URLs (who requested, what file, TTL) and retain storage access logs (S3 access logs or equivalent) for forensic or billing purposes.

Frequently Asked Questions

Checklist before you go

  • Confirm your environment variables for the storage bucket and credentials are correct.
  • Decide size thresholds where you’ll proxy vs presign.
  • Test download behavior in multiple browsers and mobile devices to ensure headers and CORS behave as expected.