Connection error — four different failures wearing the same name
- One "Connection error" hides four different failures — test DNS → TCP → TLS → HTTP in order; the first broken rung is your answer.
- Any status code means the network layer is fine — go to that code's page. Only a failure before any status is a network or proxy problem.
- curl works but the client doesn't? Usually proxy environment variables (
HTTP_PROXY/NO_PROXY) read differently by each.
Connection error is not a diagnosis. It is a summary the client writes after something below HTTP failed, and four completely different failures produce the same sentence. Walk a four-rung curl ladder — DNS, TCP, TLS, HTTP — and the first rung that breaks tells you what is wrong and who can fix it. Guessing before you have climbed the ladder is how people spend an afternoon on the wrong layer.
What you're seeing
API Error: Connection error.
TypeError: fetch failed
[cause]: Error: connect ECONNREFUSED 127.0.0.1:7890
Error: connect ETIMEDOUT 203.0.113.10:443
Error: getaddrinfo ENOTFOUND gateway.example.com
The first one tells you nothing. The other three name an errno, and the errno is the answer — ENOTFOUND and ECONNREFUSED are not variations of one problem, they are two different problems that happen to be reported by the same line of code.
If your client only prints Connection error with no errno, you are not stuck. The ladder below recovers the errno by hand.
The ladder
Each rung depends on every rung above it. The first one that fails is the only one worth investigating — everything below it never ran.
| Rung | What it proves | Failure looks like | What it means | Where to go next |
|---|---|---|---|---|
| 1 · DNS | the name maps to an address | getaddrinfo ENOTFOUND, EAI_AGAIN | wrong hostname, or your resolver can't answer | fix the hostname, or fix DNS — stop here |
| 2 · TCP | that address accepts on that port | ECONNREFUSED, ETIMEDOUT | port closed, filtered, or a proxy sitting in the middle | firewall / proxy / port — steps 2 and 5 |
| 3 · TLS | the handshake completes | handshake alert, certificate error | certificate chain or an inspecting middlebox | TLS certificate errors |
| 4 · HTTP | the server answers | any status code at all | the network is fine | the page for that status code |
The last row is the one people miss. A status code — any status code, including 401, 403, 429, 500 — means all four rungs passed. If you have a number, you do not have a connection problem, and this is the wrong page.
Check in this order
1 · Rung 1 — does the name resolve?#
HOST=$(echo "https://api.9coding.com" | awk -F/ '{print $3}' | cut -d: -f1)
echo "$HOST"
getent hosts "$HOST" || nslookup "$HOST"
- An address comes back → rung 1 passed. Note the address; step 2 needs it.
ENOTFOUND/NXDOMAIN→ either the hostname is wrong, or your resolver cannot answer for it.
To split those two, ask a different resolver the same question:
nslookup "$HOST" <another-resolver-ip>
- Resolves there, not with your default → your DNS is the problem, not the endpoint. A VPN, a container's resolver, or a stale
/etc/hostsentry are the usual causes.
grep -i "$HOST" /etc/hosts
- Fails with every resolver → check the hostname character by character. A name that resolves nowhere is almost always a typo, or an internal-only name being used from outside.
2 · Rung 2 — is the port actually open?#
Use --resolve to pin the address yourself. That takes DNS out of the experiment entirely, so a failure here can only be TCP.
curl -sS -v --connect-timeout 5 --resolve "$HOST:443:203.0.113.10" \
-o /dev/null "https://$HOST/" 2>&1 | grep -E 'Trying|Connected|refused|timed out'
Replace 203.0.113.10 with the address step 1 printed.
Connected to ...→ rung 2 passed. Go to step 3.Connection refused(ECONNREFUSED) → something answered, and the answer was no. Nothing is listening on that port.Connection timed out(ETIMEDOUT) → nothing answered at all. Packets are being dropped: a firewall, a network policy, or a route that goes nowhere.
Read the address in the error message before anything else. If ECONNREFUSED names 127.0.0.1 or localhost — as in the second error block at the top of this page — then your request never went to the endpoint. A proxy environment variable is pointing the client at a local port, and whatever used to listen there is not running. That single detail resolves a large share of "it broke and I changed nothing" reports, and no amount of endpoint debugging will surface it.
Refused and timed out mean opposite things, and it is worth being blunt about it: refused is a fast, definite no; timed out is silence. A closed port answers instantly. A firewall says nothing.
3 · Rung 3 — does TLS complete?#
openssl s_client -connect "$HOST:443" -servername "$HOST" </dev/null 2>&1 | head -25
Verify return code: 0 (ok)→ rung 3 passed. Go to step 4.unable to get local issuer certificate/self-signed certificate in certificate chain→ rung 3 failed on trust, not on reachability. The connection completed and then certificate verification failed, which is a different problem from never getting there at all. See TLS certificate errors — do not keep debugging the network.- The certificate is issued to a name you don't recognise → something between you and the endpoint is terminating TLS and re-issuing it. That is a middlebox, and it is the same page.
One property worth remembering, because it lets you skip this rung entirely: TLS failures are deterministic. The same client, the same trust store, and the same certificate fail on the first attempt and on every attempt after it. If your failure is intermittent, rung 3 is not where it lives.
4 · Rung 4 — does the server answer, and which rung broke?#
This one command replaces the whole ladder once you know how to read it. curl reports the time at which each rung completed; a rung that never completed reports 0.000000.
# <model-id>: copy an id from https://api.9coding.com/v1/models
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} status=%{http_code}\n' \
https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"<model-id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
| What you get back | Which rung broke |
|---|---|
dns=0.000000 | rung 1 — the name never resolved |
dns > 0, tcp=0.000000 | rung 2 — never connected |
tcp > 0, tls=0.000000 | rung 3 — connected, handshake failed |
tls > 0, status=000 | connected and negotiated, then the request died mid-flight — usually a timeout on a slow response |
status= any real number | all four rungs passed. This is not a connection problem |
Keep the output. dns=1.9 or tcp=4.8 is not a failure but it is a warning: something is slow enough that a client with a shorter timeout than curl's will report Connection error where curl succeeds.
5 · The rung that isn't in the ladder — your client may not take curl's path#
This is the case that makes people doubt their own eyes: curl works, the client fails, on the same machine, in the same second.
The usual explanation is proxy environment variables. curl reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY. Other clients may read them, may read only the lowercase forms, may require an explicit setting instead, or may ignore them altogether — so curl and your client can be walking two different network paths.
env | grep -i -E 'proxy'
Then run rung 4 twice — once as-is, once with the proxy variables stripped — and compare:
# <model-id>: copy an id from https://api.9coding.com/v1/models
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy -u ALL_PROXY -u all_proxy \
curl -sS -o /dev/null -w 'status=%{http_code}\n' https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"<model-id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
| With proxy vars | Without proxy vars | What it means |
|---|---|---|
| works | works | the proxy is not involved in this failure |
| works | fails | the proxy is required on this network — a client that ignores the variables cannot reach the endpoint at all |
| fails | works | the proxy is breaking the request — exclude this host from it |
| fails | fails | not a proxy problem. Go back to rung 1 |
Two follow-ups worth knowing:
NO_PROXYis the most commonly mis-set of the three. Matching rules for leading dots, port numbers and wildcards differ between implementations, so an entry that works for one tool can be silently ignored by another. Test the exclusion, don't assume it — and test it on the client version you're actually running.- If a proxy variable is set to a
127.0.0.1address, everything on this machine that honours it depends on a local process staying alive. When that process stops, every honouring client fails withECONNREFUSEDat once — and every client that ignores the variable keeps working. That asymmetry looks exactly like "one app broke".
When it isn't your problem
You are looking at the network path or the endpoint — not your configuration — when all of these hold:
- rungs 1 to 3 pass from your machine, but the request still fails,
- the failure is
ETIMEDOUTrather thanECONNREFUSED(silence in transit, not a closed port), - other clients on the same network fail the same way,
- it recovers on its own without you changing anything.
Two shortcuts to save yourself an hour:
- Intermittent failures are never certificate failures. Certificates fail every time or not at all.
- The fastest test you own is a second network. A phone hotspot separates "my network" from "the endpoint" in about thirty seconds, and no amount of local log reading does it faster.
Then check the operator's status page and changelog before touching your own setup again — ours is at status.
When reporting it, include the request id — 9Coding error responses carry one in the form (request id: 2026...). That id plus the timestamp, the model name and the full error text lets the exact call be traced. A report that only says it doesn't work cannot be investigated.
If you got a status code, you are on the wrong page
A number came back means all four rungs succeeded. The connection worked; the server chose to answer that way.
- 401 → reached the server, credential refused. See 401 Unauthorized.
- 403 → reached the server, identified, and not permitted. See 403 Forbidden.
- 429 / 529 → reached the server, and it asked you to wait. See 429 Too Many Requests.
Related
- TLS certificate errors — why they fail on the first try and never retry
- 401 Unauthorized — two layers of authentication, one error message
- 403 Forbidden — authenticated but not permitted
- 429 Too Many Requests / 529 Overloaded — the server answered, and asked you to wait
- All Claude Code errors — the quick reference table