C.W.K.
Stream
Lesson 02 of 06 · published

curl — The Real Tool That Doesn't Lie

~10 min · production, curl, debugging

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"curl is on every Mac, every Linux box, every container image. It speaks every version of HTTP. It shows you what your library hides. When an API call misbehaves, curl is the first move — not the last."

The Flags You Use Daily

A handful of flags cover 90% of debugging:

  • -v — verbose. Shows request lines (>), response lines (<), and curl's connection/TLS commentary (*). Your first debugging move.
  • -i — include response headers in output. Just headers; not the connection chatter.
  • -I (capital) — HEAD request only. Get headers without downloading the body.
  • -X METHOD — use this HTTP method. -X POST, -X DELETE, etc.
  • -H 'Name: value' — add a request header. Can repeat for multiple.
  • -d 'body' — send a body. -d @file.json reads from a file. Implies POST.
  • --data-binary @file — send file bytes verbatim, no line-ending translation. Use for binary uploads.
  • -u user:pass — HTTP Basic auth (shortcut for -H 'Authorization: Basic ...').
  • -N — no buffering of curl's own output. Essential for SSE / streaming.
  • --compressed — request and decompress gzip/br. Lets you compare compressed vs uncompressed sizes.
  • -L — follow redirects automatically. Without it, you see the 3xx response and stop.
  • -o file / -O — write body to a file (named or from URL).
  • -w '%{http_code}\n' — print specific transfer info (status code, size, time) after the response.

The X-Ray Move

curl -v is the canonical "why isn't this working" tool:

# Watch every byte sent and received
curl -v -X POST https://api.example.com/users \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer abc.def.ghi' \
  -d '{"name":"Pippa"}'

# Output shape:
# *   Trying 1.2.3.4:443...               (TCP connect)
# * TLSv1.3 (OUT), TLS handshake, ...    (TLS handshake)
# > POST /users HTTP/1.1                  (request line)
# > Host: api.example.com                 (request header)
# > Authorization: Bearer abc.def.ghi     (request header)
# > Content-Type: application/json        (request header)
# > Content-Length: 17                    (request header)
# >
# > {"name":"Pippa"}                       (request body)
# < HTTP/1.1 201 Created                  (status line)
# < Location: /users/u_abc                 (response header)
# < Content-Type: application/json         (response header)
# < {"id":"u_abc","name":"Pippa"}          (response body)

You see EXACTLY what went on the wire and EXACTLY what came back. No library magic, no framework abstractions. Half of "my httpx call returns 401 but my curl returns 200" mysteries dissolve when you compare the verbose output.

When library calls misbehave, copy the request to curl FIRST. If curl works but your library doesn't, the bug is in how the library is calling the API — not in the API itself. If curl also fails, the API is at fault (or your understanding of it is). The split tells you where to look.

The Power Combos

Timing breakdown — see DNS / TCP / TLS / response phases separately:

curl -w 'dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' -o /dev/null -s https://api.example.com/

SSE stream with no buffering:

curl -N -H 'Accept: text/event-stream' https://api.example.com/stream

Test rate limiting — hammer an endpoint 100 times, count statuses:

for i in {1..100}; do
  curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/
done | sort | uniq -c

Reproduce a browser fetch — Chrome DevTools' Network panel has "Copy as cURL" — paste straight into terminal and reproduce the bug locally.

cwkPippa's curl Usage

When cwkPippa's WebUI hangs or returns 500, my first move is curl against the backend: curl -v http://localhost:8000/api/conversations/{id}. Reveals whether it's the backend dying (connection refused), CORS blocking (browser console only — curl wouldn't see CORS), auth failing (401), or actual content issue (200 + bad body). I've also Copy-as-cURL'd from Chrome DevTools countless times to reproduce a browser-only bug from the terminal where I have tools.

Code

The daily curl recipes — copy/paste ready·bash
# Daily curl recipes — copy/paste

# X-ray a single request (the canonical debug move)
curl -v https://api.example.com/users/42

# POST JSON with auth
curl -X POST https://api.example.com/users \
  -H 'Authorization: Bearer abc' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Pippa"}'

# POST a file as the body (binary-safe)
curl -X POST https://api.example.com/upload \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @./image.png

# HEAD — get headers fast, no body
curl -I https://api.example.com/users/42

# Headers only (no curl chatter)
curl -i https://api.example.com/users/42

# Follow redirects + see them happen
curl -L -v https://example.com/short-url

# Stream SSE without curl buffering its output
curl -N -H 'Accept: text/event-stream' https://api.example.com/stream
Power combos: timing breakdown + status distribution·bash
# Timing breakdown — see where the time goes
curl -w '\ndns=%{time_namelookup}\nconnect=%{time_connect}\ntls=%{time_appconnect}\nttfb=%{time_starttransfer}\ntotal=%{time_total}\n' \
  -o /dev/null -s https://api.example.com/
# Output:
# dns=0.005    (DNS lookup)
# connect=0.045  (TCP handshake — depends on RTT)
# tls=0.120    (TLS handshake)
# ttfb=0.180   (time-to-first-byte — server processing)
# total=0.190  (full body received)

# Hammer test — 100 requests, status code distribution
for i in {1..100}; do
  curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/
done | sort | uniq -c
# Output (example): 92 200, 8 429 (rate limit kicked in)
Copy as cURL — reproduce any browser request in your terminal·bash
# Chrome DevTools → terminal
# 1. Open Chrome DevTools (Cmd+Option+I on Mac)
# 2. Network tab
# 3. Right-click a request → Copy → Copy as cURL
# 4. Paste into terminal
#
# Result: the exact request the browser sent, including all headers,
# cookies, body, and timing. Reproduce browser-only bugs from the
# command line where you have curl, grep, jq, and your full toolkit.

# Example pasted result:
curl 'https://api.example.com/api/me' \
  -H 'authority: api.example.com' \
  -H 'accept: application/json' \
  -H 'cookie: session=abc123' \
  -H 'user-agent: Mozilla/5.0 ...' \
  --compressed

External links

Exercise

Pick any one HTTP bug you've recently debugged via a library call. Reproduce it with curl -v. Compare what curl sent/received versus what your library report says it sent/received. Identify the gap (a missing header, wrong method, body format, etc). Then fix the library call to match curl's working version. Bonus: use curl -w to measure where the time goes on a slow API call (DNS / TCP / TLS / server processing) and identify the slowest phase.
Hint
The library-vs-curl gap is usually a missing or wrong header — auth, content-type, accept, or some custom header the API expects. Some libraries silently add defaults you don't want (httpx adds Accept-Encoding by default; some frameworks add User-Agent strings); some omit defaults you do want. The curl X-ray makes both visible.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.