curl returns HTTP 404 but exits 0: keep the body and check the right status
By ON Lab · Sources checked November 8, 2026
curl can receive an HTTP 404 response and still exit with code 0 because the transfer completed. Use --fail-with-body when an HTTP error should produce a nonzero exit code while keeping the response body. If your rule is strictly “only HTTP 200 is acceptable,” also test the HTTP code explicitly: curl's HTTP failure option is not a strict-200 validator.
The original reader question used curl as a condition in automation: a 404 was incorrectly treated as a successful existence check. This guide separates the process result, HTTP status and response content. We tested the examples against a disposable local HTTP server with fixed JSON responses; no external API or production resource was changed.
Pick the success rule before choosing flags
| Rule you need | Check | Remaining limitation |
|---|---|---|
| The transfer itself completed | curl's exit status | A completed transfer can carry an HTTP error |
| HTTP errors should fail and their body should remain available | --fail-with-body, exit status and saved body | It is not “exactly 200” |
| Only HTTP 200 is permitted | Transfer result plus an explicit HTTP-code comparison | A 200 body can still be the wrong content |
| The requested business action really succeeded | The API's documented response and a readback of the result | A command receipt alone is insufficient |
Do not turn a generic status example into a production write request. The examples here use one GET URL. An endpoint that creates a resource may correctly return 201; rejecting that response as “not 200” would be your policy choice, not proof that the server failed.
Observe the code without mixing it into the body
In a POSIX-style shell, replace the example URL with an authorized read-only endpoint and choose a new output filename:
curl -sS --max-time 20 \
--output response.json \
--write-out 'http=%{http_code}\n' \
'https://example.com/your-read-only-endpoint'
The body goes to the file and the HTTP code is printed separately. Read the exit status immediately, before another command changes it. A JSON filename does not establish that the server actually returned JSON; inspect the content and the endpoint's contract before parsing it as an application result.
Fail on HTTP errors while keeping their explanation
curl -sS --fail-with-body --max-time 20 \
--output response.json \
--write-out 'http=%{http_code}\n' \
'https://example.com/your-read-only-endpoint'
Check curl --version and your local help if the option is rejected. The curl manual identifies --fail-with-body as introduced in 7.76.0. The older --fail option changes HTTP-error handling too, but suppresses the response body. Choose deliberately if you need an error message for diagnosis.
For the same local 404 response, our default invocation exited 0 and saved {"error":"missing"}. With --fail, it exited 22 and did not save that error body. With --fail-with-body, it exited 22 and retained the JSON body. A local 200 response returned 0. We inspected both the process return value and the actual saved content.
When your condition must accept exactly 200
This shell example keeps transfer and HTTP decisions separate. It uses one URL, no pipeline and no automatic retry. The assignment is inside an if so an expected failure can be captured even in a script that otherwise exits on errors.
url='https://example.com/your-read-only-endpoint'
if http_code=$(curl -sS --fail-with-body --max-time 20 \
--output response.json --write-out '%{http_code}' "$url"); then
curl_exit=0
else
curl_exit=$?
fi
printf 'curl_exit=%s http=%s\n' "$curl_exit" "$http_code"
if [ "$curl_exit" -ne 0 ]; then
exit "$curl_exit"
fi
case "$http_code" in
200) exit 0 ;;
*) exit 1 ;;
esac
Our local fixture returned 200, 201, 302 and 404 on separate paths. The strict check exited 0 for 200, 1 for 201, 1 for 302 and 22 for 404. That difference is intentional: HTTP errors are handled by curl, while other disallowed statuses are handled by our policy.
The example does not follow redirects. Decide whether your endpoint should redirect before adding --location; following a redirect changes which response you are evaluating. It is also a POSIX shell example, not PowerShell syntax.
Keep failures visible through the rest of the script
Do not conclude that curl succeeded from the status of a later parser or formatting command. Separating the download from content inspection makes the original return code available. If you use a pipeline, understand your shell's pipeline-status behavior and test an intentional failure rather than assuming it propagates.
A saved error body can be useful evidence but is not a successful data payload. Read it only after recording the curl and HTTP results, and do not pass a failed response to the next production step as if it were valid. Conversely, a 200 response still needs the expected body fields or content checks. Some applications return an error envelope with a successful HTTP code.
What this check cannot diagnose
A timeout, DNS problem, certificate failure and an HTTP error are different observations. Preserve the complete error and code instead of labeling every nonzero result “not found.” The curl manual also describes caveats around HTTP failure handling and authentication; this local test is not a validation of an authenticated endpoint.
If the actual response is a GitHub API 403, use the rate-limit versus permission checklist (Korean) after recording the HTTP evidence. Changing curl's exit policy will not grant access or correct an API permission problem.