agentleFS
riteshsonawane1372riteshsonawane1372/devops-skills/skills/networking/http-debugging/SKILL.md

http-debugging

Skill0 starsChanged 2 months agoApache-2.0View on GitHub

Debug HTTP failures through proxies and load balancers - what 502, 503, 504 and 499 actually mean, keepalive and idle-timeout races, retry storms, redirect loops, compression and chunked encoding problems, CORS failures, and header size or body limits. Use when an HTTP request fails or is slow and you need to know which hop produced the response.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Bad: Treat 499 as a server error and investigate the server.

    line 284
  2. Very specific:

    Bad: Test with a single curl and declare it fixed.

    line 288
  3. Distinctive:

    Bad: Add a retry to work around intermittent 502s.

    line 296

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

HTTP Debugging

Work out which hop generated the response, then what it was reacting to.

Purpose

Use this when:

  • A request returns 502, 503, 504, 499, 413, 431, or an unexpected redirect.
  • Requests work directly against the backend but fail through the proxy.
  • Requests fail intermittently at a regular interval.
  • Latency is high with no errors, or a response is truncated.

Do not use this when:

  • The name does not resolve — use dns-troubleshooting.
  • The TLS handshake fails — use tls-debugging. A handshake failure never produces an HTTP status.
  • The problem is Kubernetes Service or endpoint routing — use kubernetes-networking.

What this skill assumes: curl, and ideally access to the proxy's access log. The access log is the authority on what the proxy did; client-side evidence alone is often ambiguous.

Operating Procedure

  1. Identify which hop answered. A 502 from your CDN and a 502 from your ingress are different incidents. Response headers usually name the responder.
  2. Reproduce with curl, with timings. The timing breakdown localises the phase immediately.
  3. Bypass hops one at a time. Client → CDN → load balancer → ingress → service → pod. Test directly against each and find where behaviour changes.
  4. Read the status code for what it actually means, not for what it is commonly assumed to mean.
  5. Check both ends of any timeout or keepalive pair. Most intermittent HTTP failures are a mismatch between two configured values.
  6. Fix the mismatch and verify with a loop, not a single request.

The status code and headers you saw are Observed. Which component set them is an Assumption until a header or an access log confirms it.

Investigation

Step 1 - curl with timings

curl -sS -o /dev/null -D - -m 30 -w '
dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect}
ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code} size=%{size_download}
' https://<host>/<path>

The phase boundaries map directly onto layers:

PhaseSlow means
time_namelookupDNS
time_connect - time_namelookupTCP handshake: network path, SYN backlog
time_appconnect - time_connectTLS handshake
time_starttransfer - time_appconnectThe server thinking — application time
time_total - time_starttransferResponse transfer: body size, bandwidth, streaming

A fast connect with a slow ttfb is an application problem and not a network one. That single distinction resolves a large fraction of "the network is slow" reports.

-D - dumps the response headers, which usually identify the responder: Server: nginx, via: 1.1 google, x-envoy-upstream-service-time, cf-ray, x-amzn-requestid.

Step 2 - what the status codes actually mean

CodeGenerated byActual meaning
502 Bad GatewayThe proxyThe proxy reached an upstream and got something invalid, or the connection closed mid-response
503 Service UnavailableThe proxy or the appThe proxy has no healthy upstream, or the app is deliberately shedding load
504 Gateway TimeoutThe proxyThe upstream accepted the request and did not respond in time
499 Client Closed RequestnginxThe client gave up first. Not a server error — usually the client timeout is shorter than the server's work
413 Payload Too LargeThe proxyBody exceeds client_max_body_size or equivalent
431 / 400 with no bodyThe proxyHeaders too large — typically a large cookie or JWT
421 Misdirected RequestThe serverConnection reuse sent a request to a server that does not serve that host
426 / 505The serverProtocol version mismatch

The 502/503/504 distinction is the most useful thing in this skill:

  • 503 means the proxy had nowhere to send it. Look at the upstream health check and the endpoint list — usually not an application bug at all.
  • 502 means it sent it and got something wrong back. Look at the backend process: it crashed, closed the connection, or spoke the wrong protocol.
  • 504 means the backend took it and went quiet. Look at the application's own latency.

499 is worth calling out because it is routinely misfiled as a server error. It means the client disconnected before the response. A wave of 499s usually means an upstream timeout shorter than this service's processing time — and often that the client is now retrying, adding load.

Step 3 - bypass hops

# Directly at the pod, bypassing Service, ingress and LB
kubectl port-forward pod/<pod> 8080:8080 &
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8080/<path>

# At the Service from inside the cluster
kubectl run -it --rm curl --image=curlimages/curl --restart=Never -- \
  curl -sS -o /dev/null -w '%{http_code}\n' http://<svc>.<ns>:8080/<path>

# At the ingress, skipping DNS and the CDN
curl -sS -o /dev/null -w '%{http_code}\n' --resolve <host>:443:<ingress-ip> https://<host>/<path>

--resolve is the most useful flag here: it forces curl to a specific IP while keeping the correct Host header and TLS SNI, letting you test one hop without changing DNS or /etc/hosts.

The hop at which behaviour changes is the hop that owns the problem.

Step 4 - keepalive and timeout mismatches

This is the cause of most intermittent HTTP failures, and the signature is distinctive: a small percentage of requests fail, at random, with no pattern in the payload.

The race: the proxy keeps an idle connection in its pool. The backend's idle timeout is shorter, so the backend closes it. The proxy, not yet aware, sends a request onto the closing connection. The result is a 502 or a reset.

The rule: the backend's idle timeout must be longer than the proxy's.

# nginx / ingress-nginx
grep -rE 'keepalive_timeout|proxy_read_timeout|proxy_connect_timeout' /etc/nginx/
kubectl get configmap -n ingress-nginx ingress-nginx-controller -o yaml

# What the backend advertises
curl -sSI https://<host>/ | grep -i 'keep-alive\|connection'

Typical defaults worth checking against each other:

ComponentSettingCommon default
AWS ALBidle timeout60s
nginxkeepalive_timeout75s
Node.jsserver.keepAliveTimeout5s — shorter than every proxy above it
Go net/httpIdleTimeoutunlimited unless set
Envoyidle_timeout1h

The Node.js default of 5 seconds behind a 60-second ALB is a well-known source of intermittent 502s, and the fix is to raise the application's value above the proxy's.

The same ordering logic applies to request timeouts, from the outside in: client > CDN > LB > ingress > application. When an inner timeout is longer than an outer one, the outer hop gives up first and the work is wasted — and the client typically retries, multiplying the load.

Step 5 - the proxy's own log

kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=200 | grep -E ' (50[234]|499) '

The access log tells you what the proxy chose and what happened, which no client-side observation can. Look for the upstream address (which backend), the upstream status (what it said), and the upstream response time (how long it took). A 502 with an empty upstream status means the connection failed before any response — a completely different problem from a 502 with upstream status 500.

Decision Tree

HTTP request fails
├── 503 → the proxy has no healthy upstream
│   ├── Endpoints empty     → kubernetes-networking
│   └── Health checks failing → the health endpoint or the app
├── 502 → invalid or closed upstream response
│   ├── Intermittent, low rate  → keepalive race: raise the backend idle timeout
│   ├── Constant                → backend crashed, or wrong protocol (HTTPS backend as HTTP)
│   └── On large responses only → buffer size, or header limits
├── 504 → upstream too slow
│   ├── App p99 exceeds the proxy timeout → fix the app; do not just raise the timeout
│   └── Upstream never accepted            → connection saturation
├── 499 → the client gave up first
│   └── The caller's timeout is shorter than this service's work; check for retries
├── 413 / 431 → size limits on body or headers
├── Works direct, fails via proxy
│   ├── Host header rewritten by the proxy
│   ├── Path rewritten or stripped
│   └── Protocol mismatch (h2 vs h1, TLS to a plaintext backend)
└── Intermittent with no pattern
    ├── Keepalive race
    ├── One unhealthy backend out of N → loop the request to find it
    └── Connection pool exhaustion at the proxy

Common Causes

CauseSignalFixRisk
Backend idle timeout < proxy'sRandom low-rate 502sRaise the backend's above the proxy'smutating
Health check path wrong503, endpoints presentCorrect the health checkmutating
Timeout ordering inverted504 at the edge, work completes laterOuter timeouts longer than innermutating
Retry without backoffLoad rises after a blipBackoff with jitter; cap attemptsmutating
Host header rewritten404 via proxy, 200 directPreserve the Host headermutating
Large JWT in a cookie431 or a bodyless 400Raise header buffers; shrink the tokenmutating
Compression on a streaming responseTruncation or bufferingExclude the endpoint from compressionmutating
One bad backend in the pool~1/N of requests failRemove it; fix the health check that missed itmutating

Safety

Read-only

curl with GET or HEAD, reading proxy configuration, reading access logs. Note that a GET is only read-only if the endpoint is — do not assume, and never use curl to exercise an endpoint you have not read the handler for.

curl --resolve changes only your own request's routing and is safe.

Potentially disruptive

  • Changing proxy timeouts or keepalive settings — affects every request through that proxy. Applying a config reload drops nothing on nginx (nginx -s reload is graceful), but a bad configuration fails the reload. Validate with nginx -t or the equivalent first.
  • Reloading or restarting an ingress controller — a reload is graceful; a restart drops every in-flight connection. With a single replica, a restart is an outage.
  • Enabling or disabling compression, buffering, or HTTP/2 — changes behaviour for all traffic, and can break streaming or long-polling endpoints that worked by accident.
  • Load testing through the proxy — you are adding load to the component you suspect. Bound it, and do not do it during an incident.
  • Raising a body-size limit — accepts larger uploads, with memory and disk implications at the proxy.

Destructive

This skill proposes no destructive operations. Be aware that using curl to reproduce a failing request against an endpoint that mutates state — a POST, PUT, PATCH or DELETE — performs that operation for real. Reproduce against a non-production environment, or against an endpoint you have confirmed is idempotent and safe.

Verification

Before: 2.1% of requests return 502, no pattern; nginx log shows upstream closed connection
After:  0 of 5000 requests return 502; Node keepAliveTimeout 65s > ALB idle timeout 60s
for i in $(seq 1 500); do
  curl -sS -o /dev/null -w '%{http_code}\n' https://<host>/<path>
done | sort | uniq -c

curl -sS -o /dev/null -w '%{http_code} %{time_starttransfer}\n' --keepalive-time 120 \
  https://<host>/<path> https://<host>/<path> https://<host>/<path>

Two requirements specific to HTTP:

  1. Loop it. A 2% failure rate passes a single check 98% of the time. Run at least 500 requests for anything intermittent.
  2. Reuse connections. Passing multiple URLs to one curl invocation reuses the connection, which is what exercises the keepalive path. A fresh connection per request will never reproduce a keepalive race — which is exactly why these bugs survive testing.

Also confirm at the hop that reported the problem: the proxy access log should show the error rate falling, not just your client.

Anti-Patterns

Bad: Raise the timeout to fix 504s. Better: A 504 says the backend is slow. Raising the timeout converts fast failures into slow ones, holds connections open longer, and hides the regression while the user waits longer.

Bad: Treat 499 as a server error and investigate the server. Better: 499 means the client disconnected first. Look at the caller's timeout — and at whether it is now retrying and doubling your load.

Bad: Test with a single curl and declare it fixed. Better: Intermittent HTTP failures are the majority of real ones. One request proves nothing; loop it, and reuse the connection.

Bad: Restart the ingress controller when requests fail. Better: That drops every in-flight connection cluster-wide. Read the access log first — it usually contains the answer.

Bad: Add a retry to work around intermittent 502s. Better: Retries hide the cause and multiply load during partial failures. Fix the keepalive mismatch; if you do add retries, they need backoff, jitter and a cap.

Bad: Assume 502 means the application crashed. Better: 502 covers "the connection closed", "the response was malformed" and "the protocol was wrong" as well. The proxy's upstream status field distinguishes them in one line.

Composition

  • tls-debugging — handshake failures, which never reach HTTP.
  • dns-troubleshooting — when time_namelookup is the slow phase.
  • kubernetes-networking — 503s with empty endpoints.
  • linux-network-debugging — resets, timeouts, and conntrack beneath HTTP.

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.