Sending Data
So far every request has been a GET: "show
me." Sending is the other half of the web —
forms, API calls, uploads — and in curl it
all flows from one option. The lab's /echo
endpoint exists for this chapter: it answers
with a JSON report of exactly what arrived,
so every claim below is checkable against
the server's own account.
-d makes a POST
$ curl -s -d 'name=specimen&count=2' http://127.0.0.1:8901/echo
{
"method": "POST",
"path": "/echo",
"content_type": "application/x-www-form-urlencoded",
"headers": {
"Host": "127.0.0.1:8901",
"User-Agent": "curl/8.5.0",
"Accept": "*/*",
"Content-Length": "21",
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "name=specimen&count=2",
"form_fields": {
"name": "specimen",
"count": "2"
}
}
Adding -d (long form --data) does three
things at once: it switches the method to
POST, it puts your string in the request
body, and it labels the body with the
Content-Type of an HTML form,
application/x-www-form-urlencoded — the
encoding browsers use when a form's fields
are joined as name=value&name=value. The
server parsed those two fields back out and
reported them under form_fields. (All the
echo captures in this chapter except this
first one are trimmed to the lines that
change; the header block repeats.)
Multiple -d flags join with &, and the
variations matter:
| Option | Difference that bites |
|---|---|
-d / --data |
sends the string as-is; @file reads a file |
--data-urlencode |
percent-encodes its value first |
--data-raw |
like -d, but a leading @ stays literal |
--data-binary |
@file contents sent untouched |
Let curl do the encoding
That form encoding has rules: spaces, &,
and , inside a value must be escaped, or
the server reads your data as structure.
Hand-escaping is error-prone; --data-urlencode
does it:
$ curl -s --data-urlencode 'name=J R R Tolkien & Sons' --data-urlencode 'city=Mesquite, TX' http://127.0.0.1:8901/echo
"body": "name=J+R+R+Tolkien+%26+Sons&city=Mesquite%2C+TX",
"form_fields": {
"name": "J R R Tolkien & Sons",
"city": "Mesquite, TX"
}
The wire body shows the escapes (+ for
spaces, %26 for the & that is data, not
a separator, %2C for the comma), and the
server's parsed fields show the values back
intact. If you send values with -d
unencoded, the & in "Tolkien & Sons"
becomes a field boundary and your data
arrives in pieces — silently, because the
request is perfectly legal; it's just not
the request you meant.
JSON: the body and the label are separate jobs
APIs usually want JSON. -d will happily
carry it — from a file with @:
$ curl -s -d @payload.json http://127.0.0.1:8901/echo
"content_type": "application/x-www-form-urlencoded",
"body": "{\"count\":2,\"name\":\"specimen\",\"tags\":[\"lab\",\"example\"]}",
The JSON arrived byte-perfect — but labelled
as a form. Many APIs reject that outright;
lenient ones parse the body and never tell
you the label was wrong. -d sets the form
label by default no matter what you feed it,
so declaring JSON is a second, explicit job:
$ curl -s -d @payload.json -H 'Content-Type: application/json' http://127.0.0.1:8901/echo
"content_type": "application/json",
"body": "{\"count\":2,\"name\":\"specimen\",\"tags\":[\"lab\",\"example\"]}"
-H adds (or replaces) a header. Rule of
thumb worth taping to the monitor: curl
moves bytes faithfully and labels them
conservatively; if the bytes are JSON, say
so. The same pattern sends data typed
inline: -d '{"name":"specimen"}' with the
same -H — mind your shell's quoting, single
quotes outside, double quotes inside.
The @file newline trap
-d @file reads the file but strips
carriage returns and newlines — a courtesy
designed for one-line data files, and a trap
for everything else. The lab's two-line
upload.txt, sent that way:
"body": "A specimen file for upload practice.It contains nothing but these two lines.",
One line, the line break gone, the sentences
run together. When the bytes must be exact,
use --data-binary @file instead.
Real uploads: -F
File uploads in forms are a different
encoding, multipart/form-data, where each
field is its own section and files travel as
attachments. -F builds it:
$ curl -s -F 'note=@upload.txt' -F 'kind=specimen' http://127.0.0.1:8901/echo
"content_type": "multipart/form-data; boundary=------------------------2PC2Rn2tkyUbw9h0yaTONE",
"form_fields": {
"kind": "specimen"
},
"files": {
"note": {
"filename": "upload.txt",
"size": 77
}
}
Two -F options: note=@upload.txt (the
@ again meaning "from this file", now with
its name attached) and a plain field,
kind=specimen. curl invented the boundary
string in the Content-Type, packed both
parts, and the server unpacked them: field
under form_fields, file under files with
its filename. This is the shape photo
uploads, document submissions, and most
"attach a file" forms on the web still use.
Other methods, and data going up a GET
Occasionally an API wants another method —
PUT, DELETE, PATCH. -X names it:
$ curl -s -X PUT -d 'replaced=true' http://127.0.0.1:8901/echo
"method": "PUT",
-X changes the word and nothing else; the
-d still makes the body and (by itself)
would have implied POST. Prefer letting -d,
-F, and -I imply their methods, and keep
-X for the methods nothing implies.
And when data belongs in the address
instead — a query string — -G moves -d
family data up there, encoded:
$ curl -G --data-urlencode 'q=hello world' http://127.0.0.1:8901/json -o /dev/null -v
> GET /json?q=hello+world HTTP/1.1
Only the request line is shown (the rest of
-v is chapter 3's subject; the JSON body
went to /dev/null). The request went out
as a GET with ?q=hello+world attached.
Note that the full URL now contains your
data — which servers log, a point chapter 5
returns to.
Check: against /echo, your POST reports
method POST and your fields parsed back;
--data-urlencode round-trips a value with
spaces and an & intact; your JSON reports
application/json once you've added the
header; and -F reports your file under
files with its name. If any field arrives
split or re-labelled, the echo has just
shown you the exact moment it happened.
Next: proving who you are — and where the proof can leak.