Web Services

Debugging REST APIs End-to-End with curl: Methods, Headers, Auth, Upload, and Diagnosis

curl is the first scene of most API debugging; this guide starts from "see exactly what a request sent", covering GET/POST, query vs body, Bearer/JWT auth, Multipart uploads, and reading status code plus timing from one response.

By LaoHand Team·7 min read·Updated 2026-09-06

Learn to see through yourself: -v and -i reveal a whole request

The beginning of debugging is not guessing but seeing exactly what you sent and received. curl -v prints the request line, request headers, TLS handshake, and response headers in full; -i shows the response headers rather than only the status code. Together they are the watershed between "the API rejected me" and "my request is malformed".

Watch a few fields closely: whether Content-Type matches the body, whether Authorization rides under the right header name, and whether Host matches the address. This one-liner shows both directions at once.

curl -v https://api.example.com/v1/users -H "Authorization: Bearer $TOKEN"
# 只看响应头
curl -sI https://api.example.com/v1/users
# 若要 -v 的详实又要输出干净,可重定向 body
curl -v -o /dev/null https://api.example.com/v1/users

Method, query, and body: no data in GET, right Content-Type for POST

Under REST semantics, GET only retrieves and carries nothing of substance (filters belong in the query string after ?); POST puts its payload in the body. Two classic problems follow: hard-joining query filters onto a POST path so routing fails, or POSTing JSON without declaring application/json so the server parses a pile of undefined out of a form body.

In curl, a JSON body sets Content-Type with -H and sends a string with -d; --data-urlencode handles forms/query with special characters. Below are the three in contrast: GET query, POST+JSON, POST+form.

GET
curl -sG https://api.example.com/v1/users --data-urlencode "page=1" --data-urlencode "name=张"
POST JSON
curl -s -X POST https://api.example.com/v1/users \
  -H "Content-Type: application/json" \
  -d '{"name":"alice","active":true}'
POST form
curl -s -X POST https://api.example.com/v1/login \
  -d "username=alice&password=secret"

Auth trifecta: Bearer, Basic, and cookie sessions

Modern APIs predominantly use Bearer tokens (JWT), which in curl is just the header Authorization: Bearer <token>; Basic is username:password in base64, best carried with -u user:pass; and for session-bearing web endpoints use -c to save cookies and -b to send them.

Two classic auth debugging traps: debugging with a stale token that has already expired, and sending Authorization twice so the backend reads a dirty value. A sound habit is stashing the token in a variable and referencing it per request, which prevents typos and eases swapping tenants.

Bearer
TOKEN=$(curl -s -X POST https://auth.example.com/token -d "grant_type=client_credentials" -H "Authorization: Basic Zm9vOmJhcg==" | jq -r .access_token)
curl -s https://api.example.com/me -H "Authorization: Bearer $TOKEN"
Basic
curl -s -u alice:secret https://api.example.com/private
Cookie
curl -s -c /tmp/ck.txt -d "user=a&pass=b" https://app.example/login
curl -s -b /tmp/ck.txt https://app.example/profile

Uploads and files: the right multipart/form-data shape with progress

File uploads use multipart/form-data; curl specifies the field name and local path with -F and generates the boundary and Content-Type itself, so hand-writing a boundary is always unnecessary trouble.

Flatten several -F for a multi-field, multi-file request; for big files use --progress-bar to watch progress, and --limit-rate to keep an upload from saturating the link. One more gotcha: quote paths containing spaces.

单文件
curl -X POST https://api.example.com/files \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/report.pdf" -F "note=for review"
多文件带进度与限速
curl --progress-bar --limit-rate 5M \
  -F "a=@a.zip" -F "b=@b.zip" \
  -H "Authorization: Bearer $TOKEN" \
  https://api.example.com/archive
# 只上传字节流而非文件(用分号或 < 重定向)
cat body.json | curl -d @- https://api.example.com/parse

Diagnose responses: status code, timing, and redirects in one read

When "the request went out but the result is wrong", read the response itself: -w hands you http_code and multi-segment timing (DNS, TCP, TLS, starttransfer, total) in one shot. Slow-DNS vs slow-handshake vs slow-body transfer each demand very different countermeasures.

Automatically follow redirects with -L, or a 301/302 returns just an empty jump page. Combining -w with -o /dev/null is the standard move to align real backend latency with the vague "the front feels slow". Below is a full timing probe.

curl -s -o /dev/null -w "http=%{http_code} \nDNS=%{time_namelookup}s TCP=%{time_connect}s TLS=%{time_appconnect}s TTFB=%{time_starttransfer}s total=%{time_total}s \nredirs=%{num_redirects} url=%{url_effective}\n" \
  -L https://api.example.com/v1/orders
# 只看重定向链 不拉 body
curl -sIL https://api.example.com

Mistake vs fix: stop guessing at certificate and encoding errors

Self-signed or internal https servers raise SSL certificate problem: self-signed; the correct remedy is importing the CA locally or adding --cacert—flinging -k as a brute-force bypass is a discouraged backstop that also opens the door to MITM.

Another frequent offender is garbage output, usually the server returns gzip while you dump it raw to text: use --compressed so curl decompresses, otherwise you debug mojibake and never find the root cause. Convert both cases from guessing to the right flag.

# 错误示范:关闭校验证书绕坑
# curl -k https://internal.example /api

# 修复对照:用自家 CA 或临时 cacert
curl --cacert /etc/ssl/certs/ca.pem https://internal.example/api
# 错误示范:不解压存乱码
# curl -s https://api.example/export > out.txt
# 修复:--compressed 自动解压
curl --compressed -s https://api.example/export > out.txt

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.