We've all been there: you open Chrome DevTools, click Copy as cURL, paste it into your terminal, and it works like a charm.
Then you rewrite it into Python requests or httpx, hit run, and suddenly:
HTTP 400 Bad Request
{"error": "Malformed JSON payload"}
Or worse, a silent HTTP 403 Forbidden.
I spent an hour yesterday debugging an internal webhook call that worked in bash but kept failing in our automated runner. Here are the 4 subtle edge-cases where translating cURL to Python breaksβand how to fix them.
1. The Trailing Slash & Automated Redirect Trap
When you run this in terminal:
curl -X POST https://api.example.com/v1/auth -d '{"user":"test"}'
If the endpoint expects https://api.example.com/v1/auth/ (with a trailing slash), modern curl silently follows or handles it depending on server config.
In Python:
# β Silently drops POST body if server returns a 301/308 redirect!
r = requests.post("https://api.example.com/v1/auth", json={"user": "test"})
By default, standard requests.post() will follow a 301/302 redirect by downgrading the method to GET and dropping the payload entirely!
Fix: Always verify whether the server enforces a trailing slash. If you need automatic redirect preservation, use a Session or inspect r.history.
2. Double-Encoded JSON String vs. Raw Dict
In curl, people often write:
curl -X POST https://api.example.com/data \
-H "Content-Type: application/json" \
-d '{"items": [1, 2, 3], "active": true}'
When porting to Python, junior devs often do this:
# β Double-stringification trap
import json
import requests
payload = json.dumps({"items": [1, 2, 3], "active": True})
requests.post(url, json=payload)
Notice the mistake? Passing json=json.dumps(...) serializes the string twice. The server receives a string literal instead of a JSON object.
Rule of thumb:
- Use
json=my_dict(requests handles serialization and headers for you). - OR use
data=json.dumps(my_dict)with explicitheaders={'Content-Type': 'application/json'}. Never mix both.
3. The Pseudo-Headers Copied from Chrome
When you click "Copy as cURL" from browser DevTools, Chrome copies everything, including HTTP/2 pseudo-headers:
curl 'https://service.com/api' \
-H 'sec-ch-ua: "Chromium";v="128"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-origin' \
-H 'Accept-Encoding: gzip, deflate, br, zstd'
If you blindly paste all those headers into Python:
- Some WAFs (like Cloudflare or Akamai) detect that the TLS fingerprint does NOT match a real browser Chrome TLS handshake, and they flag the request as a spoofed bot.
- If
Accept-Encoding: br(Brotli) orzstdis sent, Python'srequestslibrary cannot decode it natively unless you havebrotliinstalled, leaving you with raw binary gibberish inr.text.
Fix: Strip out browser-specific sec-ch-* headers and let Python handle encoding headers naturally. Keep only Authorization, Content-Type, and your custom headers.
4. Escaped Quotes in Bash Shells
If your curl payload contains shell variables or nested quotes:
curl -d "{\"title\": \"John's Report\"}" https://api.com
Bash string escaping rules differ wildly from Python string escaping. Unescaping backslashes by hand on a 50-line payload is a recipe for syntax errors.
How I Handle This Now
After hitting these gotchas one too many times during staging tests, I stopped doing manual string surgery.
If you just want clean Python Requests code without spending 15 minutes stripping pseudo-headers and fixing JSON quotes, you can drop your curl command into this in-browser converter:
π DevOmniTools cURL to Python & Fetch Converter
It runs 100% in your local browser memory (zero server uploads, so no private API tokens leak into cloud logs) and cleans up the headers automatically.
Hope this saves someone a headache next time a "working" cURL command refuses to run in production scripts!
Top comments (0)