Introduction to File Uploads and Downloads
Transferring files over HTTP introduces concerns that ordinary JSON exchange does not: large payloads, network interruptions, memory pressure, and a wide security surface. Handling them well means picking the right mechanism for the file size and use case, leaning on HTTP’s built-in features for partial transfers, and streaming rather than buffering. This guide covers upload strategies, download patterns, and the pitfalls that most often cause trouble.
1. Multipart Uploads
For the common case — a file sent alongside metadata — multipart/form-data (RFC 7578, which obsoletes RFC 2388) is the standard. The body is split into parts, each with its own Content-Disposition header.
POST /v1/documents HTTP/1.1Host: api.apiguide.devContent-Type: multipart/form-data; boundary=----boundary123
------boundary123Content-Disposition: form-data; name="title"
Quarterly report------boundary123Content-Disposition: form-data; name="file"; filename="report.pdf"Content-Type: application/pdf
%PDF-1.7 ...binary...------boundary123--Each part must carry a form-data disposition with a name; file parts should also supply a filename. A part’s Content-Type defaults to text/plain if omitted. Multiple files for one field are sent as separate parts sharing the same name. This method suits files roughly in the 1MB–100MB range.
2. Presigned (Direct-to-Cloud) Uploads
For larger files or high volume, presigned URLs offload the transfer from your application servers. The flow:
- The client asks your API for a temporary, time-limited upload URL.
- Your API returns a presigned URL scoped to a cloud storage bucket (for example S3), without exposing credentials.
- The client
PUTs the file directly to that URL. - The client notifies your API that the upload finished.
PUT /bucket/report.pdf?X-Amz-Signature=... HTTP/1.1Host: storage.example-cloud.comContent-Type: application/octet-stream
...binary...This is ideal for files over 100MB. Generate presigned URLs with the minimum necessary permissions and short expiration times to limit misuse.
3. Resumable Uploads
When large uploads may be interrupted, split the file into chunks and let the client resume from the last acknowledged chunk. The emerging draft-ietf-httpbis-resumable-upload specification formalizes this with an “upload resource” and dedicated headers:
| Header | Purpose |
|---|---|
Upload-Offset |
Bytes already processed by the server |
Upload-Complete |
Whether the whole upload is finished |
Upload-Length |
Total size of the representation in bytes |
Upload-Limit |
Server constraints such as max-size, min-size, max-append-size |
A server can send an interim 104 Upload Resumption Supported status to signal support and report progress. Note that RFC 7231 forbids a PUT carrying a Content-Range header — a server must respond 400 Bad Request — because PUT replaces a whole resource. Partial writes belong to PATCH or purpose-built methods.
For very small files (under ~1MB), Base64 embedding in a JSON body is an option, but it inflates the payload by about 33% and does not scale to larger files.
4. Range Requests for Downloads
Range requests let clients fetch specific byte ranges, which powers resumable downloads and media seeking.
The server advertises support with Accept-Ranges: bytes. The client then sends a Range header:
GET /v1/videos/large.mp4 HTTP/1.1Host: api.apiguide.devRange: bytes=0-1023A successful partial response returns 206 Partial Content with a Content-Range header describing the bytes sent. An out-of-bounds range returns 416 Range Not Satisfiable. bytes is the only range unit formally defined by RFC 7233.
HTTP/1.1 206 Partial ContentAccept-Ranges: bytesContent-Range: bytes 0-1023/2097152Content-Length: 10245. Streaming and Content-Disposition
Stream, do not buffer
The most common download pitfall is loading an entire file into memory before sending it — this exhausts RAM and can crash the server under concurrency. Instead, read the file in small chunks and write them straight to the response, keeping memory usage flat regardless of file size. Use asynchronous streaming and, where possible, a production static-file middleware rather than a hand-rolled endpoint per file.
Content-Disposition
On downloads, the Content-Disposition response header controls client handling:
Content-Disposition: attachment; filename="report.pdf"; filename*=UTF-8''report.pdfinline(the default) displays the content in the browser.attachmentprompts a download, using the suggested filename.filename*uses RFC 5987 encoding for non-ASCII names and is preferred overfilenamewhen both are present.
6. Security and Error Handling
Security
- Validate by magic bytes, not the client-supplied
Content-Type— an executable can masquerade as an image. - Rate limit upload endpoints to blunt denial-of-service and abuse.
- Scan uploads for malware before serving them back.
- Sanitize filenames and paths to prevent path traversal (for example
../../etc/passwd); note that URL-encoded sequences like..%2fcan slip past naive stripping. - Keep presigned URLs least-privilege and short-lived.
Error handling
Use standard status codes and a consistent, structured JSON error body:
| Status | When |
|---|---|
400 Bad Request |
Malformed request |
413 Content Too Large |
File exceeds size limits |
415 Unsupported Media Type |
Disallowed file type |
500 Internal Server Error |
Server-side failure |
Because a direct file response can obscure errors from the browser, download UIs sometimes fetch via JavaScript (inspecting status and Content-Disposition) so failures can be surfaced without a full page navigation.
In short: match the mechanism to the file size, prefer streaming and standard HTTP semantics over custom schemes, and treat every upload as untrusted input.