Answer the question first
In a jQuery $.ajax error callback, jqXHR.status is a numeric HTTP status code *only* when the browser actually receives a response from the server. When the request is blocked by the browser’s Same‑Origin Policy or a CORS failure, the browser reports a network error and sets jqXHR.status to 0. That 0 is not sent by the server; it is a signal that no HTTP response reached the client.
When does this happen?
- CORS rejection – The server does not send
Access‑Control‑Allow‑Origin (or it does not match the request’s origin), or the preflight OPTIONS response is missing required headers.
- Network‑level failure – DNS resolution fails, the TCP handshake is aborted, or the connection is reset before any data arrives.
- Aborted request – The page navigates away,
jqXHR.abort() is called, or the browser terminates the request for any reason. The callback still runs with status 0.
- Local file or unsupported scheme – Requests from
file:// or other non‑HTTP schemes are treated as network errors and return status 0.
Why the status is reliable when CORS is satisfied
If the server includes a correct Access‑Control‑Allow‑Origin header (or * for public APIs) and, when withCredentials:true is used, also Access‑Control‑Allow‑Credentials:true, the browser forwards the real HTTP status code from the server. In that scenario jqXHR.status will be 200, 404, 500, etc., exactly as the server sent it.
Server‑side logging for unambiguous matching
To correlate a client‑side error event with the exact server response, structure each log entry with these fields:
| Field | Description |
| timestamp | ISO 8601 UTC time of the request |
| request_id | Unique ID (e.g., UUID or trace‑parent) generated per request |
| method | HTTP method (GET, POST, etc.) |
| uri | Full request path and query string |
| remote_ip | Client IP address |
| status_code | HTTP status returned to the client |
| response_size | Bytes sent in the response body |
| error_message | Server‑side error description (if any) |
Include the request_id in the X‑Request‑ID response header. The client can log that ID in the console when jqXHR.status is 0, then the server logs can be filtered by the same ID to find the original attempt.
When the error callback fires despite an aborted request
The error callback is invoked for any non‑successful outcome, including abort. The jqXHR.status remains 0, and jqXHR.statusText is typically "abort". If you need to distinguish a user‑initiated abort from a CORS or network error, check jqXHR.statusText === 'abort' or use jqXHR.state() === 'abort' in jQuery 3.x.
Minimal steps to diagnose and fix the issue
- Open DevTools → Network and trigger the AJAX call. Verify the status column: a value of 0 with a “CORS” or “blocked” message indicates a policy block.
- Check the console for a CORS error message: “No ‘Access‑Control‑Allow‑Origin’ header is present.”
- Inspect the server response headers (or use
curl -I) to confirm the presence of Access‑Control‑Allow‑Origin and, if necessary, Access‑Control‑Allow‑Credentials and Access‑Control‑Allow-Methods.
- Update the server’s CORS policy to match the request origin or set
* for public APIs. Ensure that any custom headers or methods trigger a preflight that the server accepts.
- If
withCredentials:true is used, verify that the server sends Access‑Control‑Allow‑Credentials:true and that the Access‑Control‑Allow‑Origin header is not * but the exact origin.
- Add a
request_id header to responses and log it on the server. In the client error callback, log that ID so you can filter server logs for the exact request.
One diagnostic detail that would change the recommendation
Could you confirm whether the AJAX request was sent with withCredentials:true or with any custom headers that would trigger a preflight? Knowing that would determine if the CORS failure is due to missing Access‑Control‑Allow‑Credentials or a preflight rejection, and the fix would shift from adding a simple Access‑Control‑Allow‑Origin to also configuring Access‑Control‑Allow‑Headers and Access‑Control‑Allow-Methods.