Linux and Bash for Developers · Environment, Networking and Remote Access
Debugging HTTP Calls from the Command Line
Tell apart connection refused, timeouts, TLS and HTTP errors with curl, and make scripted calls fail loudly.
Scenario
A deploy script calls an internal API to register the new version. The deploy reports success, but the service never shows up. Later it hangs for minutes when the API is down. Nobody can tell whether the problem is DNS, the network, or the API.
What you see in production
- Scripts that "succeed" even though the API returned 500.
- Scripts that hang for minutes on unreachable hosts.
- Vague errors like "it can't connect".
The naive code
curl http://registry.internal/api/register -d "version=1.4.0"
echo "registered"
Reproduce it
curl's exit status is 0 for HTTP errors by default, and without --max-time a slow server can hold it for a long time:
cat > api.py <<'EOF'
import http.server, time
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/slow":
time.sleep(5)
code = 500 if self.path == "/broken" else 200
self.send_response(code); self.end_headers(); self.wfile.write(b"response\n")
def log_message(self, *a): pass
http.server.HTTPServer(("127.0.0.1", 18433), H).serve_forever()
EOF
python3 api.py & server=$!
sleep 0.5
curl -s http://127.0.0.1:18433/broken > /dev/null; echo "HTTP 500, curl exit status: $?"
curl -s http://127.0.0.1:18499/ > /dev/null; echo "nothing listening, curl exit status: $? (connection refused)"
kill $server
Output
HTTP 500, curl exit status: 0 nothing listening, curl exit status: 7 (connection refused)
Run this on your computer — see “Get Started” for setup.
Root cause
curl treats any HTTP response, including 4xx and 5xx, as a successful transfer, so scripts must check status codes themselves. Without timeouts, an unresponsive server or a dropped connection can block for a long time.
The fix
Fail on HTTP errors, set connect and total timeouts, retry transient failures, and print diagnostics:
cat > api.py <<'EOF'
import http.server, time
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/slow":
time.sleep(5)
code = 500 if self.path == "/broken" else 200
self.send_response(code); self.end_headers(); self.wfile.write(b"response\n")
def log_message(self, *a): pass
http.server.HTTPServer(("127.0.0.1", 18434), H).serve_forever()
EOF
python3 api.py & server=$!
sleep 0.5
call() {
curl --fail-with-body --silent --show-error --connect-timeout 2 --max-time 1 \
--write-out 'status=%{http_code} time=%{time_total}s\n' -o /dev/null "$1" 2>&1 \
| sed -E 's/time=[0-9.]+s/time=<measured>/'
}
call http://127.0.0.1:18434/ok
call http://127.0.0.1:18434/broken || echo "-> failed with exit status $?"
call http://127.0.0.1:18434/slow || echo "-> failed with exit status $?"
getent hosts localhost
kill $server
Output
status=200 time=<measured> curl: (22) The requested URL returned error: 500 status=500 time=<measured> curl: (28) Operation timed out after 1001 milliseconds with 0 bytes received status=000 time=<measured> ::1 localhost
Run this on your computer — see “Get Started” for setup.
--fail-with-body(or-f) makes HTTP errors exit non-zero;--show-errorprints why.--connect-timeoutand--max-timebound waiting;--retry 3 --retry-all-errorshandles blips (only for idempotent calls).-vshows DNS, TLS and headers;--write-outreports status and timings.- Debug layer by layer: name resolution (
getent hosts), TCP (nc -vz host port), TLS (openssl s_client -connect host:443), then HTTP.
Trade-offs
- Retries on non-idempotent POSTs can register things twice; use idempotency keys or don't retry.
- Very short timeouts cause false failures under load; base them on the API's normal latency.
-k(skip TLS verification) hides real problems; fix certificates instead.
Detect it in production
- Scripts should log the status code and timing of every call.
- Alert on deploy steps that take longer than usual.
- Synthetic checks against internal APIs catch outages before deploys do.
Key points
- curl exits 0 on HTTP errors unless you use --fail.
- Always set connect and total timeouts in scripts.
- Debug DNS, TCP, TLS and HTTP separately.
Further reading: curl man page · Everything curl
Practice questions
Why can a script with curl report success when the API returned 500?
Answer
curl exits with status 0 for any completed HTTP transfer; use --fail or --fail-with-body to make 4xx/5xx responses fail.
Which curl options prevent scripts from hanging?
Answer
--connect-timeout for establishing the connection and --max-time for the whole transfer.
How do you tell a DNS problem from a connection problem?
Answer
Resolve the name first (getent hosts / dig); if it resolves, test the TCP port (nc -vz host port); connection refused means nothing is listening.
Open in the app to mark lessons complete, take notes and track your progress.