[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fp3p45ydk35z":3},{"_id":4,"slug":5,"title":6,"subtitle":7,"kind":8,"cards":9,"tags":59,"categories":61,"source":63,"lang":66,"author":67,"audioState":70,"stats":71,"publishedAt":74,"renderer":75},"6abb8e35ca21c797c7e9d128","why-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395","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.","news",[10,14,19,24,29,34,39,44,49,54],{"headline":11,"body":12,"imageUrl":13,"sourceImageUrl":13},"Why your cURL command works in terminal but fails in Python (4 gotchas that…","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: Or worse, a silent HTTP 403 Forbidden.","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiztoy68froch4erz5v92.jpg",{"headline":15,"body":16,"imageUrl":17,"images":18},"I spent an hour yesterday debugging an internal","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:","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F1.webp",{"local":17},{"headline":20,"body":21,"imageUrl":22,"images":23},"If the endpoint expects https:\u002F\u002Fapi.example.com\u002Fv1\u002Fauth\u002F (with a trailing","If the endpoint expects https:\u002F\u002Fapi.example.com\u002Fv1\u002Fauth\u002F (with a trailing slash), modern curl silently follows or handles it depending on server config.","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F2.webp",{"local":22},{"headline":25,"body":26,"imageUrl":27,"images":28},"By default, standard requests.post() will follow a 301\u002F302","By default, standard requests.post() will follow a 301\u002F302 redirect by downgrading the method to GET and dropping the payload entirely!","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F3.webp",{"local":27},{"headline":30,"body":31,"imageUrl":32,"images":33},"Fix: Always verify whether the server enforces a","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: When porting to Python, junior devs often do this:","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F4.webp",{"local":32},{"headline":35,"body":36,"imageUrl":37,"images":38},"Notice the mistake? Passing json=json.dumps(...) serializes the string","Notice the mistake? Passing json=json.dumps(...) serializes the string twice. The server receives a string literal instead of a JSON object. Use json=my_dict (requests handles serialization and headers for you). OR use data=json.dumps(my_dict) with explicit headers={'Content-Type': 'application\u002Fjson'}. Never mix both. 3. The Pseudo-Headers Copied from Chrome When you click \"Copy as cURL\" from browser DevTools, Chrome copies everything, including HTTP\u002F2 pseudo-headers: If you blindly paste all those headers into Python:","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F5.webp",{"local":37},{"headline":40,"body":41,"imageUrl":42,"images":43},"Some WAFs (like Cloudflare or Akamai) detect that","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.","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F6.webp",{"local":42},{"headline":45,"body":46,"imageUrl":47,"images":48},"If Accept-Encoding: br (Brotli) or zstd is sent","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.","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F7.webp",{"local":47},{"headline":50,"body":51,"imageUrl":52,"images":53},"Fix: Strip out browser-specific sec-ch-* headers and let","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:","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F8.webp",{"local":52},{"headline":55,"body":56,"imageUrl":57,"images":58},"Bash string escaping rules differ wildly from Python","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. After hitting these gotchas one too many times during staging tests, I stopped doing manual string surgery.","\u002Fapi\u002Fmedia\u002Fposts\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-b1cea395\u002F9.webp",{"local":57},[60],"dev",[62],"Technology",{"name":64,"url":65},"Dev.to","https:\u002F\u002Fdev.to\u002Fdevomnitools\u002Fwhy-your-curl-command-works-in-terminal-but-fails-in-python-4-gotchas-that-wasted-my-afternoon-3o1c","en",{"handle":68,"displayName":69},"spots","Spots","queued",{"views":72,"likes":73,"saves":73,"shares":73,"completions":73,"opens":73,"skips":73,"depthSum":73},3,0,"2026-09-29T10:08:53.029Z","local"]