Part of: Browser-Based Developer Tools: Why Client-Side Beats Uploading

From Copy as cURL to Production Code, Without the Copied Secrets

A cURL command being converted into equivalent JavaScript fetch, Python requests and Go net/http code

The workflow is universal. A request works in the browser. You need it to work in your code. So you open DevTools, right-click the request, choose Copy as cURL, and now you have a 1,400-character command with nineteen headers in it.

Translating that by hand is dull and error-prone, and the thing that usually goes wrong is not the translation — it is what comes along with it.

Read the command before you convert it

A browser’s Copy as cURL is a faithful reproduction of what the browser sent. That includes every header it attached automatically: the full Cookie jar for that origin, Authorization, CSRF tokens, sec-ch-ua client hints, Referer, and a User-Agent string identifying your exact browser build.

Most of that is noise your code does not need. Some of it is a live credential.

The flags that actually carry meaning:

FlagMeans
-X, --requestHTTP method
-H, --headerA header; repeats
-d, --data, --data-rawRequest body; repeated flags join with &
-u, --userBasic auth as user:password
-G, --getSend the data as a query string instead of a body
-b, --cookieCookie header
-L, --locationFollow redirects
-k, --insecureSkip TLS verification
-F, --formMultipart form field; @ prefixes a file

Two behaviours catch people out. First, -d implies POST: a command with a body and no -X is a POST, so a “GET” you wrote with -d was never a GET. Second, -G moves the data into the query string, which is how you send parameters with -d syntax on an actual GET.

Mapping onto each language

The translation is mostly mechanical, but each ecosystem has one thing it wants done its own way.

JavaScript fetch. Headers go in a headers object, the body goes in body as a string — fetch will not serialise an object for you, so a JSON body needs JSON.stringify. And fetch does not reject on a 4xx or 5xx; it only rejects on a network failure. Checking response.ok is not optional:

const response = await fetch(url, { method: 'POST', headers, body });
if (!response.ok) throw new Error(`Request failed: ${response.status}`);

Axios. Takes data rather than body, serialises a plain object to JSON on its own, sets Content-Type for you, throws on non-2xx by default, and follows redirects without being asked. Passing a JSON.stringifyed string here is a common and harmless-looking mistake that produces a double-encoded body.

Python requests. The distinction that matters is json= versus data=. json= serialises the object and sets the content type; data= sends a form encoding. Booleans and nulls become True and None, which is why a body copied verbatim from a cURL command will not parse as Python.

Python httpx. Near-identical to requests in spirit, with async support and one behavioural difference worth knowing: it does not follow redirects by default, so -L maps to an explicit follow_redirects=True.

Go net/http. The most verbose. The body must be an io.Reader, so a string body gets wrapped in strings.NewReader. Headers are set individually with req.Header.Set, basic auth has a dedicated req.SetBasicAuth, and the response body must be closed — defer resp.Body.Close() — or you leak connections.

The part that ends up in a commit

This is the failure mode worth designing against. The copied command contains a bearer token. The generated snippet contains it too. The snippet gets pasted into a file, the file gets committed, and now a credential is in the repository history — where deleting it in a later commit does not remove it.

Three defences, in order of how much they help:

  1. Strip what you do not need. Client hints, Referer and User-Agent are almost never required. Fewer headers means fewer places for a secret to hide.
  2. Mask before sharing. If the snippet is going into a ticket, a pull request or a message, replace the credential first. Keeping the scheme visible — Bearer … — preserves the diagnostic information without the secret.
  3. Read secrets from the environment. os.environ["API_TOKEN"], process.env.API_TOKEN, os.Getenv("API_TOKEN"). A generated snippet is a starting point, not the final shape of the code.

If the token in your command is a JWT and you want to know what it grants before deciding how carefully to handle it, decoding it locally with the JWT Decoder will tell you its scopes and expiry — covered in more detail in Debugging JWTs Client-Side.

Doing it without a server

Most online cURL converters post your command to a backend. Given that the command routinely contains a session cookie and an Authorization header, that is the one category of tool where the convenience is least worth it.

The cURL to Code converter parses and generates entirely in the browser, with a toggle that masks tokens and passwords in the output before you copy it. Same result, no upload, and nothing to rotate afterwards.

← All articles