October 19, 2026
I Stopped Reading Error Messages as Errors
How HTTP status codes, TCP resets, timeouts, and application exceptions reveal what is happening beneath the surface.

By Fateyaly
11 min read
The first thing I learned about debugging systems was that an error message rarely tells the whole story.
A request returns 403 Forbidden. Is the user unauthorized? Did a reverse proxy block the request? Is a Web Application Firewall enforcing a rule? Or did the application deliberately deny access after evaluating the user's permissions?
The browser says ERR_CONNECTION_RESET. Did the server crash? Did a firewall terminate the connection? Did a proxy interrupt it?
And when a request simply times out, where exactly did the time disappear?
These situations may look similar to someone staring at a failed request. Technically, however, they can represent entirely different failure modes.
The useful question isn't just "Why did this fail?" It's "At which layer did the expected behavior stop?"
Once I started treating errors as evidence instead of obstacles, troubleshooting became less about guessing and more about eliminating hypotheses.
Let's examine how that works.
1. An HTTP Error Is Not a Network Error
Consider this request:
curl -i https://example.com/api/profilecurl -i https://example.com/api/profileSuppose the server responds:
HTTP/2 403
content-type: application/json
{"error":"access_denied"}HTTP/2 403
content-type: application/json
{"error":"access_denied"}The request reached a component capable of returning an HTTP response. That is useful evidence.
It does not, however, prove that the application itself generated the response. A CDN, reverse proxy, API gateway, WAF, or application server could have produced it.
Now compare it with:
curl: (28) Operation timed outcurl: (28) Operation timed outHere, curl did not receive a response within its configured timeout. The cause could be a routing problem, a silently dropping firewall, an overloaded server, a stalled upstream dependency, or a connection that was established but never completed the expected exchange.
These failures require different investigations.
Observation What it tells you What it does not prove
401 Unauthorized Authentication is required or failed according to the responding component That the credentials themselves are incorrect
403 Forbidden The request was refused by a component enforcing access rules That the user lacks permission in the application specifically
404 Not Found The requested resource was not found, or its existence was concealed That the endpoint does not exist
500 Internal Server Error A server-side component encountered an internal failure That the application code, rather than an upstream component, caused it
Connection refused A connection attempt was actively rejected That the host is completely unavailable
Connection timeout An expected connection or response did not arrive in time That the server is downObservation What it tells you What it does not prove
401 Unauthorized Authentication is required or failed according to the responding component That the credentials themselves are incorrect
403 Forbidden The request was refused by a component enforcing access rules That the user lacks permission in the application specifically
404 Not Found The requested resource was not found, or its existence was concealed That the endpoint does not exist
500 Internal Server Error A server-side component encountered an internal failure That the application code, rather than an upstream component, caused it
Connection refused A connection attempt was actively rejected That the host is completely unavailable
Connection timeout An expected connection or response did not arrive in time That the server is downThe distinction matters during security testing, too.
A 404 might indicate a nonexistent route, but some applications intentionally return 404 for resources the current user cannot access. A 403 might originate from the application, or it might be an edge-security rule rejecting the request before the application sees it.
An observed status code is a fact. Its cause is a hypothesis.
That separation is one of the most useful habits a technical investigator can develop.
2. The TCP Connection Can Tell a Different Story
HTTP describes application-level communication. TCP operates below it, establishing and maintaining a reliable byte stream.
When a connection fails, the transport behavior can provide clues that an HTTP response never will.
Consider three common outcomes.
Connection refused
A typical command:
curl --connect-timeout 3 http://127.0.0.1:8080/curl --connect-timeout 3 http://127.0.0.1:8080/Might return:
curl: (7) Failed to connect to 127.0.0.1 port 8080curl: (7) Failed to connect to 127.0.0.1 port 8080On a conventional TCP connection attempt, this may correspond to a TCP reset because nothing is listening on the destination port, or because a network device actively rejects the connection.
Check the local listening sockets:
ss -lntpss -lntpOr on Windows:
netstat -anonetstat -anoOn Windows, netstat -ano can show listening ports and associated process IDs. Use Task Manager or an appropriate process-inspection command to investigate the PID.
But don't jump to conclusions. A service may be listening only on 127.0.0.1, while the client is connecting to the machine's LAN address. A container may expose a port internally without publishing it externally. A service may also be bound to a different interface or port.
The question becomes: Is the service absent, or is it listening somewhere other than the path I'm testing?
Connection reset
A reset indicates that a TCP connection was abruptly rejected or terminated. Depending on where it occurs, it can originate from an endpoint or an intermediary.
Possible causes include:
- A service or proxy terminating the connection unexpectedly.
- A firewall configured to actively reject traffic.
- An application closing a connection in an unexpected state.
- A protocol mismatch or connection-handling issue.
A reset is not a diagnosis. It is a clue about how the connection ended.
If you have authorization to inspect the traffic, a packet capture can help establish where the reset appears in the exchange.
With Wireshark, useful filters include:
tcp.flags.reset == 1tcp.flags.reset == 1To focus on a particular conversation, first identify its IP addresses and TCP ports, then narrow the capture to that flow.
A reset observed at the client does not automatically reveal who generated it. Network topology, packet timing, and endpoint logs help establish the likely source.
Connection timeout
A timeout is often more ambiguous.
The client might be sending SYN packets without receiving a response. Alternatively, the TCP handshake may complete successfully, but the server might take too long to return HTTP data.
Those are different failure points.
For example:
curl -v --connect-timeout 3 --max-time 10 \
https://example.com/api/reportcurl -v --connect-timeout 3 --max-time 10 \
https://example.com/api/reportHere:
--connect-timeout 3limits the time allowed to establish the connection.--max-time 10limits the total duration of the operation.-vprints diagnostic information about connection setup and protocol activity.
If the connection is established quickly but the overall request times out, investigate the response path and server-side processing before blaming DNS or TCP connectivity.
If the connection itself never completes, examine name resolution, routing, firewall behavior, and the destination's reachability.
A crucial caveat: a verbose curl trace is useful evidence, but it is not a complete packet-level trace. When the distinction matters, correlate it with packet captures and server-side telemetry.
3. DNS Can Fail Before Your Application Gets a Chance
Imagine that an API suddenly becomes unavailable.
You test the endpoint and receive:
Could not resolve host: api.example.comCould not resolve host: api.example.comIt is tempting to classify this as an application outage. But the request may never have reached an application server.
The hostname must first be resolved to an IP address.
On Linux:
dig api.example.comdig api.example.comOr:
getent hosts api.example.comgetent hosts api.example.comOn Windows:
nslookup api.example.comnslookup api.example.comThese commands provide different perspectives. dig queries DNS directly, while getent follows the host-resolution mechanisms configured on the system. nslookup is useful for inspecting DNS answers, but it does not necessarily reproduce every application's resolver behavior.
If DNS resolution fails, investigate:
- Whether the hostname is correct.
- Whether the configured resolver is reachable.
- Whether the DNS record exists and is published as expected.
- Whether a local cache or split-DNS configuration changes the answer.
- Whether the failure affects one machine, one network, or multiple environments.
That last question is particularly useful.
If a hostname resolves on a workstation but not inside a container, the problem may be specific to the container's resolver configuration. If it resolves on one corporate network but not another, split-horizon DNS or network-specific configuration may be involved.
Now consider the opposite case: DNS resolution succeeds, but the application still fails.
That moves the investigation forward. It does not establish that DNS is completely healthy in every respect, but it reduces the likelihood that simple hostname resolution is the immediate cause.
A successful DNS lookup tells you where the name resolved. It does not tell you whether the resulting service is reachable or healthy.
4. The Most Misleading Error Is Often HTTP 500
A 500 Internal Server Error is easy to misinterpret.
Developers often associate it with an exception in application code. That is a reasonable starting point, but the response alone does not establish the cause.
Imagine a reporting endpoint:
GET /api/reports/monthlyGET /api/reports/monthlyIt works for small reports but returns 500 when a customer requests a large date range.
Several explanations are possible:
- The application encounters an unhandled exception while processing the data.
- A database query takes too long or exhausts available resources.
- An upstream service returns an unexpected response.
- A serialization step fails on a particular record.
- A reverse proxy translates an upstream failure into a generic server error.
The right next step is not to rewrite the endpoint immediately. It is to correlate the failed request with evidence from the relevant components.
A simplified investigation might look like this:
Client request
|
v
Reverse proxy / API gateway
|
v
Application handler
|
v
Database or upstream serviceClient request
|
v
Reverse proxy / API gateway
|
v
Application handler
|
v
Database or upstream serviceThe response may have been triggered by a failure at any point in that path.
Start by recording the request timestamp, endpoint, response status, and a request or correlation ID if one is available. Then inspect the corresponding proxy, application, and dependency logs.
For example, a structured application log might contain:
{
"request_id": "req-7f3a",
"route": "/api/reports/monthly",
"status": 500,
"duration_ms": 8124,
"error_type": "DatabaseTimeout"
}{
"request_id": "req-7f3a",
"route": "/api/reports/monthly",
"status": 500,
"duration_ms": 8124,
"error_type": "DatabaseTimeout"
}This example is illustrative, not a real production log. Its value is the correlation: the same request can be traced across the components that handled it.
If the database logs show a timeout at the same moment, the investigation has a stronger lead. If the application logs show a serialization exception after the database query completes, the likely failure point changes.
Do not expose raw stack traces, SQL errors, internal hostnames, or sensitive diagnostic details to ordinary clients. Keep detailed diagnostics in appropriately protected logs, and return a controlled error response to the caller.
A useful security principle follows: the client needs a safe explanation of failure; the investigator needs enough protected evidence to identify its cause.
5. Read HTTP Status Codes as Part of a Sequence
One response can be ambiguous. A sequence of responses often reveals much more.
Suppose an API behaves like this:
GET /api/profile
-> 401 Unauthorized
POST /api/login
-> 200 OK
GET /api/profile
-> 200 OKGET /api/profile
-> 401 Unauthorized
POST /api/login
-> 200 OK
GET /api/profile
-> 200 OKThat sequence suggests the resource was accessible after the authentication state changed. It does not prove that every authorization rule is correct, but it gives you a testable hypothesis about the authentication flow.
Now imagine a different sequence:
GET /api/orders/123
-> 200 OK
PATCH /api/orders/123
-> 403 Forbidden
GET /api/orders/123
-> 200 OKGET /api/orders/123
-> 200 OK
PATCH /api/orders/123
-> 403 Forbidden
GET /api/orders/123
-> 200 OKThe application allows the resource to be read but rejects the modification. That may be entirely correct: read and write permissions are often different.
The important question is whether the behavior matches the intended policy.
When investigating access-control issues in an authorized test environment, compare requests across clearly defined roles and permissions. Record which operation succeeds, which fails, and whether the observed behavior matches the documented policy.
Do not treat every 403 as a vulnerability, or every 200 as proof of safe behavior. A successful response can still contain unauthorized data; a failed response can still disclose information through its body, timing, or headers.
The status code is one observation in a larger set of evidence.
6. A Timeout Can Be a Performance Problem, or a Security Signal
Timeouts deserve special attention because they can reveal problems that don't produce a clean exception.
Consider an endpoint that retrieves account information:
GET /api/accounts/123/summaryGET /api/accounts/123/summaryUnder normal conditions, it responds in 100 milliseconds. Under a particular workload, response times rise to 8 seconds and eventually requests begin timing out.
Possible causes include:
- A slow database query.
- Connection-pool exhaustion.
- Lock contention.
- An overloaded upstream service.
- Excessive synchronous work in the request handler.
- A resource-exhaustion condition caused by legitimate or abusive traffic.
The symptom alone cannot distinguish these causes.
Start by measuring latency and failure rates over time. If available, compare application traces, database query timings, CPU utilization, memory pressure, connection-pool metrics, and upstream-service latency.
A useful distinction is between latency and availability.
A service can be reachable and return valid responses while becoming so slow that it is effectively unusable. Conversely, a service can be fast when it responds but fail to respond to a subset of requests.
During authorized security testing, unusual latency patterns may justify further investigation, but they do not by themselves prove a denial-of-service vulnerability or any other security flaw. Repeatable measurements, controlled test conditions, and evidence of the underlying cause matter.
Avoid aggressive tests against production systems. Use a controlled environment, an approved test window, and agreed traffic limits when investigating resource-exhaustion behavior.
7. Build a Failure Timeline Instead of Collecting Random Clues
Here is a practical workflow for an endpoint that is failing intermittently.
Step 1: Reproduce the behavior
Use a small, controlled number of requests:
curl -sS -o /dev/null \
-w 'status=%{http_code} dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s first_byte=%{time_starttransfer}s total=%{time_total}s\n' \
https://example.com/api/statuscurl -sS -o /dev/null \
-w 'status=%{http_code} dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s first_byte=%{time_starttransfer}s total=%{time_total}s\n' \
https://example.com/api/statusThis reports several timing measurements:
time_namelookup: time until name resolution completes.time_connect: time until the TCP connection is established.time_appconnect: time until the TLS handshake completes for HTTPS.time_starttransfer: time until the first response byte is received.time_total: total operation time.
These are cumulative timing measurements, not independent durations. For example, time_starttransfer includes earlier connection setup time. Compare the fields across requests to identify which stage appears to be contributing most to the delay.
A response with a high time_starttransfer but a low connection-setup time points toward a delay after connection establishment, such as server processing or upstream work. It does not tell you which component is responsible without additional evidence.
Step 2: Identify the failure boundary
Ask these questions in order:
- Did the hostname resolve?
- Did the TCP connection establish?
- Did TLS negotiation complete?
- Did the server return an HTTP response?
- Did the application complete the intended operation?
- Did the client receive and correctly interpret the result?
This sequence prevents you from investigating application exceptions when the request never reached the application, or blaming the network when the connection was established and the delay occurred during backend processing.
Step 3: Correlate the evidence
Use the same timestamp and request ID across client output, proxy logs, application logs, and dependency telemetry where possible.
Check whether the failure is isolated to one request, one user, one host, one network path, or one dependency.
Step 4: Change one variable at a time
If you change DNS settings, proxy configuration, application code, and timeout values simultaneously, you may restore service without learning why it failed.
A controlled investigation changes one relevant variable, repeats the measurement, and records the result. This makes the conclusion more defensible and reduces the risk of hiding the actual defect.
Step 5: State what the evidence supports
A strong incident note might say:
DNS resolution and TCP connection establishment completed normally. The request then spent approximately seven seconds waiting for the first response byte. Application traces show that the reporting handler was waiting on a database query during the same interval. The evidence points to database-side latency, although the specific query-plan or locking cause remains under investigation.
That is more useful than writing, "The API was slow."
It separates observation from inference and identifies what remains unknown.
8. Errors Can Reveal Information You Didn't Intend to Expose
Error handling is not only a reliability concern. It is also a security boundary.
Consider two responses to a failed database operation.
Poorly handled response:
DatabaseError:
relation customer_credentials does not exist
Query: SELECT * FROM customer_credentials ...DatabaseError:
relation customer_credentials does not exist
Query: SELECT * FROM customer_credentials ...Safer client response:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{"error":"internal_error","message":"The request could not be completed."}HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{"error":"internal_error","message":"The request could not be completed."}The second response avoids exposing internal database details to the client. The detailed exception should instead be recorded in access-controlled server logs, with secrets and sensitive personal data excluded or appropriately redacted.
Other details that may leak through error responses include:
- Internal filesystem paths.
- Framework and dependency versions.
- SQL statements and schema names.
- Service addresses and infrastructure details.
- Stack traces and debug configuration.
- Whether a particular account or resource exists.
Not every detailed error is automatically a vulnerability. Context matters: the information must be assessed for sensitivity, accessibility, and the practical impact of disclosure.
The defensive goal is not to make all errors indistinguishable. It is to give each audience the right level of information: actionable diagnostics for authorized operators and appropriately limited details for clients.
9. A Small Error-Investigation Checklist
When a request fails, I would rather answer a few precise questions than run ten unrelated commands
- Capture the exact symptom. Save the status code, client error, timestamp, and relevant request details.
- Determine the layer. Did failure occur during DNS resolution, connection setup, TLS, HTTP processing, or application execution?
- Compare timing. Is the delay in connection establishment or after the connection succeeds?
- Check the responding component. Could a proxy, gateway, CDN, or WAF have generated the response?
- Correlate logs. Use timestamps and request IDs to follow the request through the system.
- Compare expected behavior. Does the response match the intended authentication, authorization, and business rules?
- Protect diagnostic data. Keep sensitive internal details out of client-facing responses.
- Record uncertainty. Distinguish confirmed facts from plausible explanations that still require testing.
The final point is easy to overlook. A good investigation doesn't require you to know the answer immediately. It requires you to know what you have established, and what you haven't.
Final Thoughts: Stop Treating Failure as the Opposite of Information
A failed request still tells you something.
A DNS lookup failure narrows the investigation toward name resolution. A connection refusal points toward active rejection or a listener problem. A timeout tells you that an expected event did not occur within the allowed interval. An HTTP status code tells you how the responding component classified the request. An application exception may reveal the exact operation that failed, if you can correlate it with the right logs.
None of these observations tells the entire story by itself.
The real skill is connecting them without confusing correlation with causation, symptoms with root causes, or a tool's output with proof.
That mindset is useful in Linux troubleshooting, API development, incident response, penetration testing, and production engineering. It reduces guesswork and helps you explain not only what failed, but why your conclusion is justified.
The next time a request fails, don't immediately ask how to make the error disappear.
Ask what the failure has already told you.
Because sometimes the most useful part of a system is the moment it stops behaving as expected, and the evidence it leaves behind.