DEV Community

Cover image for Why your cURL command works in terminal but fails in Python (4 gotchas that wasted my afternoon)

Why your cURL command works in terminal but fails in Python (4 gotchas that wasted my afternoon)

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"}
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

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"})
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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 explicit headers={'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'
Enter fullscreen mode Exit fullscreen mode

If you blindly paste all those headers into Python:

  1. 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.
  2. If Accept-Encoding: br (Brotli) or zstd is sent, Python's requests library cannot decode it natively unless you have brotli installed, leaving you with raw binary gibberish in r.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
Enter fullscreen mode Exit fullscreen mode

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)