HTTP Cheatsheet
Range Requests and Uploads
Use this HTTP reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Advertising and requesting ranges
A server that supports partial transfers says so, and clients then ask for byte windows instead of whole files.
HEAD /video.mp4 HTTP/1.1 HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 146515 ETag: "9c1f2a"
GET /video.mp4 HTTP/1.1 Range: bytes=0-1023 HTTP/1.1 206 Partial Content Content-Range: bytes 0-1023/146515 Content-Length: 1024 ETag: "9c1f2a" Accept-Ranges: bytes
| Range form | Meaning |
|---|---|
bytes=0-1023 | the first 1024 bytes |
bytes=1024- | from byte 1024 to the end |
bytes=-500 | the last 500 bytes |
bytes=0-0 | just the first byte, a cheap support probe |
bytes=0-99,200-299 | multiple ranges, answered as multipart/byteranges |
Offsets are zero-based and inclusive on both ends, so bytes=0-1023 is 1024
bytes. Getting that off by one is the classic resumable-download bug.
Status codes and headers for ranges
| Code | When |
|---|---|
| 200 | server ignored the Range header, sent the whole body |
| 206 | partial content, Content-Range present |
| 416 | the range is outside the resource, send Content-Range: bytes */146515 |
| 412 | If-Match or If-Range validator failed |
| Header | Direction | Value |
|---|---|---|
Accept-Ranges | response | bytes, or none to decline |
Range | request | bytes=0-1023 |
Content-Range | response | bytes 0-1023/146515, or bytes 0-1023/* |
If-Range | request | an ETag or date, makes the range conditional |
If-Range is the correct way to resume: if the validator still matches the server
sends 206 and you continue, and if it does not it sends the whole resource as 200
so you restart cleanly. Without it a changed file yields a corrupt stitch of two
versions.
Only strong validators work for ranges. A weak ETag: W/"abc" cannot be used
with If-Range, because weak equivalence does not guarantee identical bytes.
Resumable downloads
# probe support curl -sI https://example.com/big.iso | grep -iE 'accept-ranges|content-length' # one explicit window curl -r 0-1023 -o head.bin https://example.com/big.iso curl -r -500 -o tail.bin https://example.com/big.iso # resume an interrupted download from wherever the local file stopped curl -C - -O https://example.com/big.iso wget -c https://example.com/big.iso # parallel chunks, then concatenate curl -r 0-9999999 -o part0 https://example.com/big.iso & curl -r 10000000-19999999 -o part1 https://example.com/big.iso & wait && cat part0 part1 > big.iso
Ranges are also what makes media seeking work. A browser video player issues
Range requests as the user scrubs, so a server without Accept-Ranges forces a
full download before playback and breaks seeking entirely.
Upload bodies: form-urlencoded vs multipart
application/x-www-form-urlencoded | multipart/form-data | |
|---|---|---|
| Shape | a=1&b=hello%20world | boundary-delimited parts |
| Binary safe | no, everything is percent-encoded | yes |
| Size overhead | ~3× for binary data | a few bytes per part |
| Per-field metadata | none | filename and Content-Type per part |
| Use for | small text fields | any file upload |
POST /upload HTTP/1.1 Content-Type: multipart/form-data; boundary=----HUb0undary Content-Length: 431 ------HUb0undary Content-Disposition: form-data; name="title" Q3 report ------HUb0undary Content-Disposition: form-data; name="file"; filename="q3.pdf" Content-Type: application/pdf %PDF-1.7 ...binary... ------HUb0undary--
The boundary must not appear anywhere in the data, the final boundary carries a
trailing --, and every line break in the framing is CRLF.
curl -F 'title=Q3 report' -F 'file=@q3.pdf' https://example.com/upload curl -F 'file=@q3.pdf;type=application/pdf;filename=report.pdf' \ https://example.com/upload # raw body, no multipart wrapper, the simplest API upload curl -X PUT --data-binary @q3.pdf \ -H 'content-type: application/pdf' https://example.com/files/q3.pdf
Filenames from a client are attacker-controlled. Never use one to build a
filesystem path, and never trust the part's Content-Type for validation, sniff
the bytes instead.
Expect: 100-continue and large bodies
For a large upload, ask before sending the payload so a rejection costs nothing.
PUT /files/big.iso HTTP/1.1 Content-Length: 4294967296 Expect: 100-continue HTTP/1.1 100 Continue ← now send the body ... or ... HTTP/1.1 413 Content Too Large ← nothing was uploaded
| Code | Meaning for an upload |
|---|---|
| 100 | proceed with the body |
| 413 | body over the server limit, reject before transfer |
| 415 | wrong or missing Content-Type |
| 417 | the server cannot honor the Expect header |
curl sends Expect: 100-continue automatically for bodies over 1 KB and waits
up to a second. Some servers and proxies never answer it, which shows as a
consistent one-second stall on every upload. Suppress it with -H 'Expect:'.
Server body limits to know when a 413 appears: nginx client_max_body_size
(default 1 MB), Apache LimitRequestBody, and framework defaults such as
Express express.json({ limit }).
Chunked and streaming uploads
When the size is unknown up front, stream it.
POST /ingest HTTP/1.1 Transfer-Encoding: chunked Content-Type: application/x-ndjson 2a {"event":"click","ts":1789000000} 2b {"event":"scroll","ts":1789000001} 0
# stream stdin, no Content-Length tail -f app.log | curl -X POST --data-binary @- \ -H 'transfer-encoding: chunked' https://example.com/ingest # server-sent events, a streaming download curl -N -H 'accept: text/event-stream' https://example.com/events
Transfer-Encoding: chunked is HTTP/1.1 only. HTTP/2 and /3 have length-prefixed
frames, so streaming needs no special header there, and sending
Transfer-Encoding over HTTP/2 is a protocol error.
Never send both Content-Length and Transfer-Encoding: chunked. That
disagreement between a proxy and an origin is exactly what request smuggling
exploits, and compliant servers reject such a message outright.
Resumable and multipart upload protocols
Plain HTTP has no standard resumable upload, so real systems build one. Three patterns dominate.
| Pattern | How it works | Used by |
|---|---|---|
| Ranged PUT | client PUTs windows, HEAD reports the received length, resume from there | custom APIs |
| Session based (tus) | POST creates an upload URL, PATCH appends at an Upload-Offset, HEAD reports progress | tus.io, Vimeo |
| Multipart object storage | initiate, upload numbered parts in parallel each returning an ETag, then complete with the part list | S3, GCS, Azure |
# tus-style flow curl -X POST https://example.com/files -i \ -H 'Upload-Length: 4294967296' -H 'Tus-Resumable: 1.0.0' # → 201 Created, Location: /files/abc123 curl -X HEAD https://example.com/files/abc123 -i \ -H 'Tus-Resumable: 1.0.0' # → Upload-Offset: 1048576 curl -X PATCH https://example.com/files/abc123 \ -H 'Tus-Resumable: 1.0.0' -H 'Upload-Offset: 1048576' \ -H 'content-type: application/offset+octet-stream' \ --data-binary @chunk.bin
For very large files the practical answer is usually a presigned direct upload to object storage, which keeps the bytes off your application servers entirely. Whatever the mechanism, make the completion step idempotent, because a client that times out after the server committed will retry.