Converting cURL to fetch and Python requests
How cURL flags map to fetch and requests, why browser requests fail where cURL works, and how to share requests without leaking tokens.
Almost every API conversation starts with a request someone else can run. Documentation shows a cURL command, your browser can copy any network request as cURL, and your application sends the same request with fetch or Python’s requests. Moving between those forms by hand is where headers go missing, bodies get double-encoded, and tokens end up pasted into chat. This guide explains how the pieces map onto each other and what to check after converting.
Anatomy of an HTTP request
Whatever the syntax, every request has the same five parts: a method (GET, POST, PUT, PATCH, DELETE), a URL including any query string, headers, an optional body, and sometimes credentials. A cURL command spells these out with flags:
curl 'https://api.example.com/v1/notes?limit=10' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
--data '{"title":"Morning notes"}'-X/--requestsets the method. Without it, cURL uses GET, or POST as soon as a body is given with-d.-H/--headeradds one header per flag.-d/--datasends a body. Note that-don its own setsContent-Type: application/x-www-form-urlencoded, so JSON APIs need an explicit JSON content type.-u user:passsends HTTP Basic authentication, which is an Authorization header containing the Base64-encoded credentials.-I/--headsends a HEAD request and prints only the response headers.
The same request in fetch
cURL to fetch turns the command above into:
fetch("https://api.example.com/v1/notes?limit=10", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "[redacted]"
},
body: "{\"title\":\"Morning notes\"}"
});Three things differ from cURL in practice. First, fetch never throws for HTTP error statuses: a 404 or 500 is a successful promise with response.ok === false, so check it explicitly. Second, the body must be a string, so objects need JSON.stringify. Third, in a browser the request is subject to CORS. If the API does not send an Access-Control-Allow-Origin header for your page’s origin, the browser blocks the response even though the same request works from a terminal. That is not a bug in your code: the call has to move to a server, or the API has to allow your origin.
The same request in Python
cURL to Python requests produces a requests.request(...) call with the same URL, headers, and body. Before using it in real code, add three things the generated snippet leaves to you:
- A timeout, such as
timeout=10. Without one,requestscan wait forever on a stalled connection. response.raise_for_status(), so 4xx and 5xx responses raise an exception instead of passing silently.- Credentials from the environment, such as
os.environ["API_TOKEN"], instead of a literal token in the source file.
If the body is JSON, you can replace data= with json= and pass a dictionary. requests then serializes it and sets the content type for you.
Going the other way: fetch to cURL
When a request from your front end fails, the fastest way to get help from a backend team or an API vendor is a cURL command they can run without your application. fetch to cURL reads a static fetch call and writes a command with long, self-describing options. It never executes JavaScript, so it only understands literal values: string URLs, object-literal headers, and JSON.stringify of a literal object. Anything computed at runtime, such as a variable or a template literal with expressions, is rejected rather than guessed.
Browser DevTools can also produce the command for you. Open the Network panel, right-click a request, and choose Copy → Copy as cURL. That copy includes every header the browser sent, including cookies, so treat it as a secret until you have removed them.
Keep credentials out of shared snippets
Pasted commands and snippets end up in tickets, chat logs, screenshots, and search indexes. All three converters redact Authorization, cookies, and similar headers by default, replacing them with [redacted]. Only turn redaction off for a local copy you intend to run yourself. If a token has already been shared, revoke it. Deleting the message does not un-share it.
A debugging checklist
- Reproduce the failing request as cURL, and confirm it fails the same way from a terminal.
- Check the method, the full URL including the query string, and the Content-Type header. A missing or wrong content type is the most common cause of “400 Bad Request” with a correct-looking body.
- Inspect the response status and headers with the HTTP response inspector. 401 means the credentials are missing or invalid, 403 means they were accepted but not allowed, and 429 means rate limiting, so check
Retry-After. - If it works in cURL but not in the browser, look for CORS errors in the console.
- Share the redacted cURL command and the response when asking for help.
The converters accept a deliberate subset of cURL: the flags listed above, quoted arguments, and one URL. Shell variables, command substitution, and file uploads with -F @file are rejected, because silently dropping them would produce a request that behaves differently from the original.
