Kali Linux on Docker · Part 11 of 16
HTTP and API Troubleshooting From Kali Docker
Series curriculum (16 lessons)
When a service is “up” but nothing works, the problem is almost always in the layers between the two containers — name resolution, the TCP connection, the HTTP exchange, or the application itself. A Kali container is a perfect disposable probe for walking that path from the outside in. This lesson teaches you to read exactly where an HTTP or API request breaks using curl and jq, so you stop guessing and start pointing at the layer that failed.
We will work against a small local lab with two services on a user-defined Docker network: web (a plain HTTP server) and api (a JSON API with a health endpoint). If you have not built that lab yet, the Docker networking lesson and user-defined networks lesson show how containers reach each other by name. Only scan, inspect, or test systems you own or have explicit permission to assess — here that means your own local containers and benign public endpoints like example.com.
The four layers of an HTTP request
Every HTTP request climbs the same ladder. When a request fails, it fails at exactly one of these rungs, and knowing which rung tells you where to look:
DNS
↓
TCP
↓
HTTP
↓
Application
- DNS — the client turns a name like
webinto an IP address. - TCP — the client opens a socket to that IP on a port (80, 443, 8080…).
- HTTP — the client sends a request line and headers; the server sends a status line, headers, and body.
- Application — the server’s code runs and returns real data (or an error) in the body.
The whole point of curl -v is that it narrates this climb out loud, so you can see which rung the request fell off.
The tools
curl — make an HTTP request
curl sends a request and prints the response body to your terminal. It is the fastest way to ask “what does this service actually return?”
# Fetch the body of the web service on the lab network
curl http://web
By default curl prints only the response body and stays quiet about everything else. That is fine when things work — but when they don’t, you want more detail, which is where the flags come in.
curl -I — headers only
-I (capital i) sends a HEAD request: the server returns its status line and headers but no body. This is the quickest way to check a status code and content type without downloading a large payload.
curl -I http://web
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html
Content-Length: 612
The first line — HTTP/1.1 200 OK — is the status code. That single number is your primary diagnostic signal, and we will lean on it heavily below.
curl -v — verbose, the full narration
-v (verbose) prints the entire conversation: DNS resolution, the TCP connect, the request headers curl sent (lines starting with >), and the response headers it received (lines starting with <). This is the command you reach for when a request fails and you do not yet know why.
curl -v http://web
We will walk through its output in detail in the next section.
jq — parse and query JSON
APIs return JSON, and raw JSON on one line is unreadable. jq pretty-prints it and lets you pull out exactly the field you care about. You pipe curl’s output into jq:
# Pretty-print the whole JSON body
curl -s http://api/health | jq
# Extract just one field
curl -s http://api/health | jq '.status'
-s (silent) tells curl to drop its progress meter so only the JSON reaches jq — otherwise the progress bar corrupts the pipe. jq '.status' selects the status key from the top-level object. jq '.' (or bare jq) reformats the whole document with indentation.
🛠️ DevOps Perspective —
curl … | jq '.field'is the backbone of quick API health checks in scripts and CI. Because both tools are in the Kali image and read from stdin, you can chain probes together without writing a line of application code.
Walking through curl -v http://web
Run the central example and read the output rung by rung. Annotated:
curl -v http://web
* Trying 172.20.0.3:80... # DNS resolved, now connecting (TCP)
* Connected to web (172.20.0.3) port 80 # TCP connection established
> GET / HTTP/1.1 # request line curl sent (HTTP)
> Host: web
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK # response status line (HTTP)
< Server: nginx
< Content-Type: text/html
< Content-Length: 612
<
<!doctype html> ... # response body (Application)
Map that against the ladder:
- DNS —
Trying 172.20.0.3:80means the namewebalready resolved to an IP. Docker’s embedded DNS on the user-defined network turned the service name into an address. If DNS had failed, curl would never have reached this line. - TCP —
Connected to web … port 80means the socket opened. The service is listening and reachable on that port. - HTTP — the
>lines are the request curl sent; the<lines are the response headers.HTTP/1.1 200 OKsays the server understood the request and is happy. - Application — the body below the blank line is the real payload the server’s code produced.
Because the request climbed all four rungs, everything is healthy. When something breaks, curl -v stops at the rung that failed — and where it stops is the answer.
🔎 Troubleshooting Tip — Read the status code together with how far
curl -vgot, and the two pin the failing layer for you. No connection line at all → DNS or TCP. A connection but a5xx→ the application errored. A4xx→ your request was wrong (bad path, missing auth). A200but wrong data → the application logic, not the plumbing. The number tells you the layer; the layer tells you which team, config, or log to look at next.
Reading HTTP status codes
The status code is a compact statement of what happened at the top of the ladder:
- 2xx (200, 201, 204) — success. The application ran and answered.
- 3xx (301, 302, 307) — redirect. Add
-Lto have curl follow it:curl -L http://web. - 4xx (400, 401, 403, 404) — your request was the problem: wrong path, missing/invalid auth, or a resource that does not exist. The connection and server are fine.
- 5xx (500, 502, 503, 504) — the server failed.
500is an application crash;502/504usually mean a proxy could not reach an upstream (a broken link deeper in the chain);503means the service is up but refusing load.
A 4xx points you at your client and inputs. A 5xx points you at the server’s logs and its own upstream dependencies.
Inspecting headers
Headers carry the metadata that explains a response. curl -I http://api/health shows you the Content-Type (is it really application/json, or an HTML error page pretending to be an API?), caching directives, and any redirect Location. A classic API bug is a 200 OK whose Content-Type is text/html — the load balancer served an error page and jq then chokes on it. Checking headers first tells you whether you even have JSON before you try to parse it.
API health checks with jq
Most APIs expose a health endpoint. Probe the lab api service and assert on the result:
# Pretty-print the health document
curl -s http://api/health | jq
{
"status": "ok",
"version": "1.4.2",
"dependencies": {
"database": "up",
"cache": "up"
}
}
# Pull out a single nested value
curl -s http://api/health | jq '.dependencies.database'
# "up"
# Combine a status code check with a body check in one call
curl -s -o /dev/null -w '%{http_code}\n' http://api/health
# 200
-o /dev/null discards the body and -w '%{http_code}\n' prints only curl’s record of the response code — ideal for a scripted check that just needs the number. jq '.dependencies.database' reaches two levels into the JSON to read whether the API’s own database dependency is healthy, which often explains a 5xx you are seeing one layer up.
🏭 Why This Matters in Production — A health endpoint that returns
200but reports"database": "down"in its body is the difference between “the service is up” and “the service works.” Load balancers and orchestrators poll these endpoints; a Kali probe lets you read the body, not just the code, when you are diagnosing why traffic is being drained from a supposedly healthy pod.
Timeouts
A request that hangs is telling you something too — usually that a packet is being silently dropped (a firewall or a wrong route) rather than actively refused. Never let a probe hang forever:
# Give up connecting after 3s; give up on the whole request after 10s
curl --connect-timeout 3 --max-time 10 http://api/slow
--connect-timeout 3 bounds how long curl waits for the TCP handshake — if it blows past this, the host is unreachable or a firewall is dropping SYN packets (note: dropping, not rejecting — a reject is instant). --max-time 10 bounds the entire operation including the response, catching an application that connects fine but never finishes answering.
When it goes wrong: three failure signatures
Re-run curl -v and watch which rung it dies on.
Connection refused — TCP layer:
curl -v http://web:8080
* Trying 172.20.0.3:8080...
* connect to 172.20.0.3 port 8080 failed: Connection refused
DNS succeeded (you got an IP), but nothing is listening on that port. The name is fine; the service is down or you have the wrong port.
Could not resolve host — DNS layer:
curl -v http://web
* Could not resolve host: web
curl never got an IP, so it never tried to connect. The name is wrong, the container is not on the same user-defined network, or Docker’s embedded DNS is not in play (default bridge networks do not give you name resolution).
TLS handshake failure — between TCP and HTTP on HTTPS:
curl -v https://api
* Trying 172.20.0.4:443...
* Connected to api (172.20.0.4) port 443
* SSL certificate problem: self-signed certificate
TCP connected, but the certificate could not be verified before any HTTP was exchanged. That is a certificate-trust problem, not an application problem.
Common Problems
Connection refused→ the TCP rung failed even though DNS worked. The service is not running, crashed, or is listening on a different port than you tried. Verify what the container actually publishes and that the process is up; try the correct port. This is never a DNS issue if you already saw an IP in theTrying …line.Could not resolve host→ the DNS rung failed; curl never got an IP. Check the spelling of the service name, confirm both containers are attached to the same user-defined network (the default bridge gives no name resolution), and inspect resolution itself. Work through the DNS troubleshooting lesson for the systematic dig-based approach.- TLS / certificate errors (
SSL certificate problem,unable to get local issuer certificate,certificate has expired) → the connection reached the server but the handshake failed before any HTTP. This is a certificate-trust or expiry problem, not the application. Do not reflexively add-kto skip verification in anything but a throwaway lab — forward straight to the TLS certificate troubleshooting lesson to diagnose it properly. 200 OKbutjqerrors with “Invalid numeric literal” → the body is not JSON. Runcurl -Iand readContent-Type; you are almost certainly getting an HTML error page from a proxy. Fix the upstream, not the parser.5xxfrom the API → the plumbing is fine; the application failed. Read the health endpoint body withjqto see which dependency is down, then check that service’s logs.
🔐 Security Note —
-k/--insecuredisables TLS certificate verification. It is convenient in a disposable lab, but a request made with-kproves nothing about who you are actually talking to — it accepts any certificate, including an attacker’s. Never bake-kinto scripts, health checks, or CI that touch anything real.
Try It Yourself
With your web and api lab containers running on a user-defined network, start a disposable Kali probe attached to the same network:
# --rm delete the probe container on exit
# -it interactive TTY so you get a shell
# --network keep the probe on the lab network so names resolve
docker run --rm -it --network lab kalilinux/kali-rolling bash
Then, inside the probe:
curl -v http://web # walk DNS → TCP → HTTP → Application
curl -I http://web # status + headers only
curl -s http://api/health | jq # pretty-print the JSON health doc
curl -s http://api/health | jq '.status' # extract one field
curl -v http://web:9999 # force a Connection refused (wrong port)
curl -v http://wbe # force a Could not resolve host (typo)
curl -v https://example.com # a real, correct TLS handshake to compare against
Read each curl -v and name the rung that succeeded or failed before reading the error text. That habit — predicting the layer, then confirming — is the whole skill.
🧪 Try It — After the refused and unresolved cases above, fix each one (correct the port, correct the name) and watch the verbose output climb one more rung each time. Seeing the ladder rebuild is the fastest way to internalize it.
What You Learned
- Every HTTP request climbs the same ladder — DNS → TCP → HTTP → Application — and a failure lives on exactly one rung.
curl -vnarrates that climb, so where it stops identifies the failing layer;curl -Igives you status and headers fast;curlalone gives you the body.- The HTTP status code plus how far
curl -vgot pinpoints the layer: no connection → DNS/TCP,4xx→ your request,5xx→ the server,200with wrong data → application logic. curl -s … | jqparses JSON API responses and health endpoints so you can assert on the body, not just the status code.- The three signatures to recognize on sight: connection refused = service down or wrong port; could not resolve host = DNS/network; TLS errors = certificate trust, handed off to the TLS lesson.
- Bound every probe with
--connect-timeoutand--max-time, and never disable TLS verification (-k) outside a throwaway lab.
Keep going: tighten your name-resolution diagnosis in the DNS troubleshooting lesson, master certificates in the TLS certificate troubleshooting lesson, review how containers reach each other in the Docker networking lesson, or see the host-level version of this material in HTTP and API troubleshooting from Kali.
Recommended Reading
- View Book on Amazon Affiliate link
Learning Kali Linux
A hands-on introduction to the Kali Linux toolset for security testing.
- View Book on Amazon Affiliate link
The Ultimate Kali Linux Book
A broad, beginner-friendly walkthrough of Kali Linux and its core toolset.
Affiliate Disclosure: Some links on this page are affiliate links. If you purchase through one of these links, DevOps AI Toolkit may earn a commission at no additional cost to you. See our affiliate disclosure.
← Back to Kali Linux on Docker Back to Kali Linux