Skip to content
DevOps AI ToolKit
Newsletter

Kali Linux on Docker · Part 11 of 16

HTTP and API Troubleshooting From Kali Docker

Difficulty: Intermediate ~16 min Part 11/16
Prerequisites: Kali Linux fundamentalsBasic Docker knowledge
Series progress11 / 16
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 web into 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 Perspectivecurl … | 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:

  1. DNSTrying 172.20.0.3:80 means the name web already 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.
  2. TCPConnected to web … port 80 means the socket opened. The service is listening and reachable on that port.
  3. HTTP — the > lines are the request curl sent; the < lines are the response headers. HTTP/1.1 200 OK says the server understood the request and is happy.
  4. 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 -v got, and the two pin the failing layer for you. No connection line at all → DNS or TCP. A connection but a 5xx → the application errored. A 4xx → your request was wrong (bad path, missing auth). A 200 but 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 -L to 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. 500 is an application crash; 502/504 usually mean a proxy could not reach an upstream (a broken link deeper in the chain); 503 means 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 200 but 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 the Trying … 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 -k to skip verification in anything but a throwaway lab — forward straight to the TLS certificate troubleshooting lesson to diagnose it properly.
  • 200 OK but jq errors with “Invalid numeric literal” → the body is not JSON. Run curl -I and read Content-Type; you are almost certainly getting an HTML error page from a proxy. Fix the upstream, not the parser.
  • 5xx from the API → the plumbing is fine; the application failed. Read the health endpoint body with jq to see which dependency is down, then check that service’s logs.

🔐 Security Note-k / --insecure disables TLS certificate verification. It is convenient in a disposable lab, but a request made with -k proves nothing about who you are actually talking to — it accepts any certificate, including an attacker’s. Never bake -k into 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 -v narrates that climb, so where it stops identifies the failing layer; curl -I gives you status and headers fast; curl alone gives you the body.
  • The HTTP status code plus how far curl -v got pinpoints the layer: no connection → DNS/TCP, 4xx → your request, 5xx → the server, 200 with wrong data → application logic.
  • curl -s … | jq parses 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-timeout and --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.

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

Related on DevOps AI Toolkit