curl Cheatsheet

Forms and File Uploads

Use this curl reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

multipart with -F

-F / --form builds a multipart/form-data body, generates the boundary, and switches the method to POST. Each -F is one part.

curl -F 'name=ada' -F 'role=admin' https://ex.com/profile
curl -F 'avatar=@photo.png' https://ex.com/profile
curl -F 'avatar=@photo.png' -F 'name=ada' https://ex.com/profile
FormPart sent
-F 'k=v'Text field k
-F 'k=@path'File upload, filename set from the path
-F 'k=<path'File contents as a text field, no filename
-F 'k=@p;type=image/png'Override the part's Content-Type
-F 'k=@p;filename=x.png'Override the advertised filename
-F 'k=@p;headers="X-A: 1"'Extra headers on that part
-F 'k=@a,@b'Two files in one multipart/mixed part
--form-string 'k=v'Literal value, @ and < not special
curl -F 'file=@report.csv;type=text/csv;filename=q3.csv' \
     https://ex.com/import
curl --form-string 'body=@mention is literal here' https://ex.com/post

Never mix -F with -d. They build mutually exclusive bodies and curl errors out rather than merging them.

Reading from stdin

A dash as the path reads stdin, which is how you upload something generated on the fly. curl cannot know the length up front, so it uses chunked transfer encoding.

tar czf - ./src | curl -F 'archive=@-;filename=src.tgz' https://ex.com/up
pg_dump db | gzip | curl -T - https://ex.com/backups/db.sql.gz

Some servers and older proxies reject chunked uploads. If you get a 411, write to a temp file first so curl can send a real Content-Length.

PUT with -T

-T / --upload-file sends the file as the whole body with PUT, no multipart wrapper. This is what object stores and WebDAV endpoints expect.

curl -T backup.tar.gz https://ex.com/files/backup.tar.gz
curl -T backup.tar.gz https://ex.com/files/     # trailing / keeps the name
curl -T '{a.txt,b.txt}' https://ex.com/files/   # two PUTs, one process

A trailing slash on the URL means curl appends the local basename. Without it, the URL path is used verbatim, so -T a.txt https://ex.com/files overwrites /files.

Field names with special characters

The -F parser treats =, @, <, ;, and , as syntax, so escape them with a backslash on the command line. Separately, curl percent-encodes field names and filenames that contain special characters when it writes the part headers. --form-escape (curl 7.81.0) switches that to backslash-escaping instead, for servers whose multipart parser does not decode percent-escapes.

curl -F 'a\,b=1' https://ex.com/f              # literal comma in the name
curl -F 'file=@"tax 2025".pdf' https://ex.com/f # quoted path with a space
curl --form-escape -F 'field\name=curl' https://ex.com/f
curl --form-string 'query=a=b&c=d' https://ex.com/f

--form-string is the safe choice whenever the value is user data, because it turns off every special meaning.

Resuming and large files

FlagPurpose
-C - / --continue-at -Resume, let curl find the offset
-C <n>Resume at byte n
--limit-rate 2MCap throughput
--max-time <s>Wall clock cap on the transfer
--expect100-timeout <s>How long to wait for 100 Continue
-H 'Expect:'Disable the 100-continue handshake
--max-filesize <n>Refuse a download larger than n
curl -C - -T big.iso https://ex.com/up/big.iso
curl -C - -O https://ex.com/big.iso          # resume a download
curl --limit-rate 2M -T big.iso https://ex.com/up/

curl adds Expect: 100-continue for bodies over 1 MB. If a server never answers it, curl stalls for one second and then sends anyway, so -H 'Expect:' removes the pause.

Verifying what was sent

Multipart bugs are almost always a wrong boundary, filename, or part Content-Type. Dump the real body rather than guessing.

curl -v -F 'f=@a.png' https://ex.com/up 2>&1 | grep -i 'content-'
curl --trace-ascii - -F 'f=@a.png' https://ex.com/up | head -40
curl -F 'f=@a.png' https://httpbin.org/post   # echoes the parsed parts

--trace-ascii - prints the full outgoing body including the boundary lines, which -v alone hides.