You copy a cURL command from Chrome DevTools. You paste it into a terminal. It fails. The browser request worked. Your replay does not. Now you are debugging a command instead of the API.
That is a common stall, not a rare one. DevTools is showing you a real request. cURL is a different client. Cookies, forbidden headers, compression, and HTTP/2 vs HTTP/1 all change the story. The goal is not to worship the copied command. It is to replay the same intent until the status code and body match.
A copied cURL is still one of the fastest ways to share a failing call with a teammate, drop it into a ticket, or turn a click in the UI into a repeatable test. You just have to know what the browser added for you, and what the shell is about to eat.
This is a practical walk through how to copy the command, what to strip, how to replay it, and how to tell whether the API changed or the copy did.
What “Copy as cURL” actually gives you
In Chrome, the Network panel can copy the request as cURL. Firefox and Safari can too. You get a long command: URL, method, headers, and sometimes a binary-looking body. It looks complete. It is a snapshot of what the browser sent, translated into flags.
It is not a perfect snapshot. Browsers refuse to let pages set some headers, then they add those headers themselves. cURL will send whatever you pasted, including things a page script could never set. That can make a replay succeed when the site would fail, or fail when the site would succeed.
The command is also written for a Unix shell by default. On Windows, the quoting is wrong until you pick “Copy as cURL (cmd)” or convert it. A single quote that was protecting a JSON body becomes a syntax error. People then “fix” the JSON and change the bug.
Treat the copied command as a starting point. The URL, method, and body are usually the truth. The header list is a mix of useful auth and noisy browser chrome.
When replaying is the right move
Replay a request when you need the same call without the UI. That includes:
- A button that triggers a POST you want to inspect
- A 403 that only happens after login
- A webhook-style payload you want to send again
- A flaky endpoint you want to hit ten times in a row
- A teammate who cannot reproduce the click path
Do not replay a request that charges a card, deletes a row, or emails a customer until you know what it does. cURL will not ask. The browser’s “are you sure” dialog is gone.
If the original call was a GET for a page of HTML, you may not want cURL at all. You wanted the XHR that loaded the data. In DevTools, filter by Fetch/XHR before you copy. Copying the document request is how you end up downloading a full HTML shell and calling it an API bug.
Strip the headers that only exist because a browser exists
Start with a copy. Then delete noise.
sec-ch-ua, sec-fetch-site, sec-fetch-mode, and sec-fetch-dest are browser client hints. Most APIs ignore them. Some WAFs get picky. If your replay fails with a bot wall and the site works in Chrome, try removing the extra Client Hints before you invent a new auth theory.
Accept-Encoding: gzip, deflate, br is usually fine. cURL will decompress if you pass --compressed. If you skip that flag, you may print garbage and think the API returned binary junk. The API returned gzip. Add --compressed or drop the encoding header so you get plain text.
Cookie headers are the sensitive part. They make the replay work, and they are a session. Do not paste them into a public ticket. If the call needs a cookie, say so and use a redacted placeholder. If you can, swap the cookie for a bearer token that is meant to be used from a server.
Referer and Origin matter when the server checks CORS or CSRF. A server-side cURL call does not use CORS. CORS is a browser rule. If the browser failed and cURL succeeds, you likely have a CORS problem, not an API outage. If cURL fails with 403 and the browser works, you are missing a CSRF token or a cookie.
Keep Authorization, Content-Type, and custom headers your app actually sets. Those are the ones that encode intent.
JSON bodies, files, and the quoting that breaks on paste
A POST with JSON is the usual case. The copied command wraps the body in quotes. Your shell then treats $, backticks, and exclamation marks as syntax. A password in a JSON field can vanish or explode depending on the shell.
Safer pattern: put the body in a file. curl --data-binary @body.json with Content-Type: application/json. You can see the file. You can format it. You can keep secrets out of your shell history.
Form uploads are different. -F sends multipart. If you turn a multipart request into -d with a JSON string, the server will not see files. If you turn JSON into -F, the server may parse fields and ignore the JSON. Match the original Content-Type.
Empty bodies on POST still matter. Some frameworks treat POST without a body as a different route. If DevTools shows {}, send {}, not nothing.
HTTP/2, redirects, and the status code you did not see
Browsers speak HTTP/2. cURL may speak HTTP/1.1 unless you ask. Most APIs do not care. A few CDNs do. If a replay gets a strange 400 from a proxy, try --http2 or the opposite. It is not the first thing to try. It is on the list after auth and body.
Follow redirects on purpose. -L follows. Without it, you may see 302 and an empty body and call the API broken. With it, you may POST to the first URL and GET the second, which is not the same as the browser’s fetch. Look at the method on each hop.
Print verbose output once. curl -v shows the request headers cURL actually sent, TLS, and the status line. Compare that to DevTools. If the path differs, you copied the wrong row. If the auth header is missing, the shell ate it. If the host differs, a redirect or environment mismatch is in play.
CORS is not a cURL error, and cURL will not prove CORS works
Developers copy cURL because the browser console said “blocked by CORS policy.” Then cURL returns 200 and they conclude the backend is fine. Both can be true. CORS is enforced in the browser when JavaScript from origin A reads a response from origin B. cURL is not a browser. It will show you whether the server answered. It will not show you whether Access-Control-Allow-Origin would have allowed the page to see that answer.
If the product fails in the browser and cURL works, inspect the response headers in cURL anyway: Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Vary: Origin. Compare them to what the page needs. A missing header is still a backend bug. It just is not a “the route 500s” bug.
Preflight is an OPTIONS request. Copying the POST will not replay the preflight. If OPTIONS is what fails, copy that row, or craft OPTIONS with the same Access-Control-Request-Method and Access-Control-Request-Headers the browser sent.
A straightforward replay workflow
Copy as cURL from the XHR that failed, not from the document. Paste into a text file, not into Slack. Remove cookie values if you are sharing. Keep the URL, method, and body.
Run it locally with -v --compressed. Confirm the status code. Confirm the body. If you need to hit staging, change the host only. Leave the path and query string until you know they are innocent.
If auth expires, do not keep replaying a stale bearer token and calling the API flaky. Get a fresh token the same way the app does. Then replay.
When it matches production, save the command in the ticket with secrets removed. That is now a regression test in disguise. You can turn it into a proper test later. You already have the method, URL, and payload shape.
How to tell the API changed versus the copy changed
Change one thing at a time. If you reformat JSON and the call fails, the server may be sensitive to key order (rare) or you dropped a field. Diff the body against the original copy.
If you switch environments, you changed more than the host. Auth, feature flags, and data will differ. A 404 on staging for an ID that exists in production is not a cURL problem.
If you run the same command twice and get two answers, you are not looking at a pure function. You posted a mutation. Idempotency keys exist for this. If the first call created a resource, the second should not look like the first. That is success, not flakiness.
Clock skew shows up here too. Signed URLs and tokens with exp fail if your machine time is wrong. The copied command was valid at capture time. Five hours later it is a museum piece.
Common mistakes that look like API outages
Backslashes from a copied command wrapped for bash, pasted into PowerShell. The URL breaks. The server never sees the query string you thought you sent.
A -H 'Content-Type: application/json' plus -d that starts with @ when you meant a file. Some versions send the literal @. Use --data-binary @file.
HTTP on a host that only speaks HTTPS. The browser upgraded. cURL did not. You get a 301 or a connection error and blame DNS.
IPv6 vs IPv4. Rare, until it is not. If the browser works and cURL hangs, try -4 and see if the hang disappears. Then fix DNS or the listen address, not the JSON.
Copying from a request that was cancelled. DevTools still shows it. The body may be incomplete. Look at the status column. (canceled) is not 200.
A few real situations where a careful replay saves the day
A checkout click returns 200 in the Network panel and the UI still errors. You replay the XHR and see {"ok": false, "reason": "inventory"}. The UI ignored the body. The copied command made the body readable in a terminal, which is a better place to read JSON than a minified preview.
A mobile webview fails login. You copy the token request from a desktop session and it works. The mobile client was sending a different User-Agent and an older API version header. The server was not down. The clients were not equal. Add the mobile headers to the replay and the 400 comes back. Now you have a fix, not a theory.
A partner says they “sent the same payload.” They send you a cURL. You run it and get 415. Their Content-Type was text/plain. The body was JSON. The copied command made that obvious because the header is sitting in the flags, not in a slide deck.
Keep secrets out of the command you keep
A useful cURL snippet in a README should use environment variables for tokens. -H "Authorization: Bearer $TOKEN". The captured command from DevTools will hard-code the session. That is convenient for five minutes and expensive if it hits git.
Rotate anything that was pasted into chat. Assume it leaked. Replay with a new credential.
If the body contains personal data, do not store the full command as the fixture. Keep the shape. Replace emails and card numbers. You can still test parsing without keeping a customer in your repo.
A simple habit that pays off
When a request fails in the browser, copy the XHR as cURL, put it in a file, strip the browser-only headers, add --compressed -v, and replay. If cURL matches the browser, you are looking at application data. If cURL succeeds and the page fails, look at CORS, CSRF, and the JavaScript that reads the response. If cURL fails and the page works, you dropped a cookie, a header, or a quote.
The command is not the product. It is a camera pointed at one call. Use it to see the call clearly, then put the evidence in a ticket without the secrets. The next person will not need your click path. They will need the URL, the method, the body, and a status code that does not lie.