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:

  • MethodGET, 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-u for HTTP Basic, or an Authorization header 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'. Type application/x-www-form-urlencoded. In fetch, build a URLSearchParams; in requests, pass data={"a": 1, "b": 2}.
  • JSON-H 'Content-Type: application/json' -d '{...}'. In fetch, JSON.stringify the body; in requests, json={...}.
  • Multipart-F field=value -F [email protected]. Type multipart/form-data with a boundary. In fetch, use a FormData object (do not set Content-Type manually — the runtime adds the boundary); in requests, pass files={...}.

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.