Reading a curl command and translating it to code
How to read a curl command field by field and translate it to fetch or Python requests correctly, including the implicit-POST gotcha that trips up most translations.
영문 본문을 표시하고 있습니다. 번역은 준비 중입니다.
API docs hand you a curl line. Your browser's network panel has a
"Copy as cURL" button. Someone pastes a reproduction in a bug report
as curl .... In every case the curl command is the canonical
description of an HTTP request, and the task is the same: read it
correctly and reproduce it in fetch, Python requests, or axios.
Most of the translation is mechanical. The part people get wrong is
the implicit behavior — the things curl does that aren't spelled out
in the flags.
The anatomy of a request
Every HTTP request has five parts, and every curl flag maps to exactly one of them:
- Method —
GET,POST,PUT, etc. Set by-X, or inferred. - URL — the bare argument, including query string.
- Headers — one per
-H. - Body —
-d,--data-raw,-F, or--data-urlencode. - Auth —
-ufor HTTP Basic, or anAuthorizationheader via-H.
Translating a curl command means reading each flag, deciding which of the five parts it sets, and emitting the equivalent in your target language. The method is the only field that's frequently implicit, which is where most mistranslations come from.
Flags reference
| Flag | Meaning | Effect on the request |
|---|---|---|
-X, --request |
Set method | Overrides the inferred method (-X POST, -X PUT) |
-H, --header |
Add a header | One header per flag; later ones can override |
-d, --data |
Request body | Implies POST + application/x-www-form-urlencoded; multiple -d joined with & |
--data-raw |
Body, literally | Like -d but does not treat a leading @ as a file reference |
--data-urlencode |
Body, percent-encoded | Percent-encodes the value before sending |
-G, --get |
Move data to query | Sends -d data as the URL query string with a GET |
-u, --user |
HTTP Basic auth | Sends Authorization: Basic <base64(user:pass)> |
-F, --form |
Multipart field | Sets multipart/form-data; -F file=@path uploads a file |
-b, --cookie |
Send cookies | Adds a Cookie header |
-L, --location |
Follow redirects | Re-issues the request on 3xx responses |
A few of these have sharp edges worth stating outright. -d strips
newlines from its argument and reads from a file when the value starts
with @ (so -d @body.json sends the file contents) — --data-raw
is the variant that sends the @ literally. --data-urlencode
percent-encodes its value, which matters when a form field contains
&, =, or spaces; the rules are the same ones covered in
URL percent-encoding. And -u
without HTTPS sends your credentials Base64-encoded over the wire,
which is encoding, not encryption (more on that below).
The implicit-POST gotcha
This is the single most common mistake when translating curl by hand:
curl https://api.example.com/items -d 'name=widget&qty=3'
There is no -X POST here, yet this is a POST. The presence of
-d changes the method from the default GET to POST, and it sets
Content-Type: application/x-www-form-urlencoded unless you override
it with a -H. People translating this command often write a GET
with a query string, or a POST with a JSON body — both wrong. The
correct reading is: POST, form-encoded body, name=widget&qty=3.
The same applies to -F (implies POST with multipart/form-data)
and -T/--upload-file (implies PUT). When you do see an explicit
-X, it wins — curl -X PUT -d '...' is a PUT with a form body.
Read the body flags first, infer the method, then let -X override.
Worked example
Take a realistic command with auth, a custom header, and a JSON body:
curl -X POST https://api.example.com/v1/orders \
-H 'Authorization: Bearer tok_abc123' \
-H 'Content-Type: application/json' \
-d '{"sku":"A-19","qty":2}'
Note that -d carries JSON only because a -H 'Content-Type: application/json' is present. Without that header, curl would label
the same bytes as form-encoded. The body content type is decided by
the header, not by the shape of the string.
In JavaScript fetch:
const res = await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer tok_abc123",
"Content-Type": "application/json",
},
body: JSON.stringify({ sku: "A-19", qty: 2 }),
});
In Python requests:
import requests
res = requests.post(
"https://api.example.com/v1/orders",
headers={"Authorization": "Bearer tok_abc123"},
json={"sku": "A-19", "qty": 2},
)
The requests version uses json=, which sets the
Content-Type: application/json header for you — so passing it
explicitly would be redundant, and the example drops it. That
convenience is also a trap: if the curl command's content type were
anything else, json= would be the wrong tool and you'd need data=.
Read the header before choosing the parameter.
Body content types
The body flags are not interchangeable; each maps to a different content type and a different parameter in your client.
- Form-encoded —
-d 'a=1&b=2'. Typeapplication/x-www-form-urlencoded. Infetch, build aURLSearchParams; inrequests, passdata={"a": 1, "b": 2}. - JSON —
-H 'Content-Type: application/json' -d '{...}'. Infetch,JSON.stringifythe body; inrequests,json={...}. - Multipart —
-F field=value -F [email protected]. Typemultipart/form-datawith a boundary. Infetch, use aFormDataobject (do not setContent-Typemanually — the runtime adds the boundary); inrequests, passfiles={...}.
The failure mode is mixing these up: sending a form-encoded body but
declaring application/json, or JSON.stringify-ing a FormData.
The server will reject the mismatch, often with a confusing 400.
Basic auth is Base64, not encryption
-u user:pass produces an Authorization: Basic <token> header where
the token is base64("user:pass"). Base64 is reversible with no key —
anyone who sees the header recovers the credentials instantly. Over
HTTPS that's acceptable because TLS encrypts the whole exchange; over
plain HTTP it is equivalent to sending the password in cleartext. If
you ever see -u against an http:// URL, treat those credentials as
compromised.
Translating it is straightforward. In fetch:
headers: { "Authorization": "Basic " + btoa("user:pass") }
In requests, pass auth=("user", "pass") and the library builds the
header for you.
When the translation isn't one-to-one
A few curl behaviors have no clean single-line equivalent. -L
(follow redirects) is the default in requests but must be requested
in fetch via redirect: "follow" (also its default) — and neither
re-sends a POST body to the redirect target by default, matching
curl's own behavior unless --post301/--post302 are set. Cookies
from -b become a Cookie header, but if the original relied on
-c/--cookie-jar to persist cookies across requests you'll need a
session object (requests.Session()) to reproduce it. And --compressed
maps to letting the client negotiate Accept-Encoding, which both
fetch and requests do transparently.
Once the request goes out, the response status code tells you whether
your translation matched — a 415 means the content type was wrong, a
401 means the auth header didn't land. Our HTTP status reference
maps each code to its likely cause.
For the common case, paste the curl command into our
curl converter and get the fetch, requests,
and axios equivalents with the method and content type inferred
correctly — including the implicit-POST case that hand translation
so often gets wrong.