How to make curl fail safely in shell scripts
A curl process exit code and an HTTP response status answer different questions. Make scripts bound their requests, preserve useful error evidence, and branch on the result they actually need.
Reviewed by the Terminaster editorial team · Updated
What you will take away
- By default, curl can exit zero after receiving an HTTP 404 or 500 response.
- Use --fail or --fail-with-body when HTTP 400-and-higher responses should make curl fail.
- Capture `$?` immediately, and do not treat HTTP code 000 as a server response.
- Add connect and total timeouts, and retry only operations whose repetition is safe.
Separate curl’s exit code from the HTTP status
The shell exit code reports whether curl completed its transfer according to the selected options. The HTTP status is application-protocol data returned by an HTTP server. A server can successfully return a 404 response, so curl normally exits zero even though the resource was not found.
Capture the exit code immediately after curl. Any intervening command replaces `$?`. An HTTP code displayed as `000` is curl’s placeholder when no HTTP response code was obtained; it is not a real status sent by a server.
Capture both results explicitly
http_code=$(curl -sS \
--connect-timeout 5 --max-time 30 \
-o response.json -w '%{http_code}' \
https://api.example/items)
curl_rc=$?
printf 'curl_rc=%s http_code=%s\n' "$curl_rc" "$http_code"Choose whether HTTP errors should fail curl
`--fail` normally makes curl return exit code 22 for HTTP responses with status 400 or greater and suppresses the response body. The curl manual warns that this is not completely fail-safe, especially around authentication responses such as 401 and 407. `--fail-with-body` returns 22 under the corresponding detected HTTP failure conditions but retains the body, which is often useful when an API returns structured error details.
Neither option decides which statuses your application may accept. For example, a cache check may treat 404 as an expected miss, while a deployment download should reject it. Encode that policy in the script. Also avoid `if ! curl; then rc=$?`: the logical negation replaces the original curl status.
Fail on HTTP errors while retaining the body
if curl -sS --fail-with-body \
--connect-timeout 5 --max-time 30 \
-o response.json \
https://api.example/items; then
printf 'request completed\n'
else
rc=$?
printf 'request failed (curl exit %s)\n' "$rc" >&2
exit "$rc"
fiPublish a response only after both checks pass
For application-specific status rules, download into a private temporary file and capture curl’s transfer result separately from the HTTP status. Move the body into its final pathname only after the transfer succeeded and the status is one your application accepts.
The example treats 404 as an expected cache miss, accepts only 200 as a usable payload, and leaves no partial response at the final pathname. Its trap removes the temporary file on normal exit and common signals.
- Exit 6 means curl could not resolve the host.
- Exit 7 means curl failed to connect to the host.
- Exit 22 is returned for an HTTP error when a fail option is active.
- Exit 28 means the configured operation timed out.
Handle an expected 404 without publishing partial data
umask 077
tmp=$(mktemp)
cleanup() { rm -f -- "$tmp"; }
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
http_code=$(curl -sS \
--connect-timeout 5 --max-time 30 \
--output "$tmp" --write-out '%{http_code}' \
https://api.example/items/42)
curl_rc=$?
if [ "$curl_rc" -ne 0 ]; then
printf 'transfer failed (curl exit %s)\n' "$curl_rc" >&2
exit "$curl_rc"
elif [ "$http_code" = 404 ]; then
printf 'item not found\n'
exit 0
elif [ "$http_code" != 200 ]; then
printf 'unexpected HTTP status %s\n' "$http_code" >&2
exit 1
fi
mv -- "$tmp" response.json
trap - EXIT HUP INT TERMBound every automated request and retry carefully
Automation should not wait forever. Set a connect timeout and a per-attempt total timeout appropriate to the service. Limit the retry count and the window in which curl may start another retry; `--retry-max-time` is not a hard wall-clock cap because an attempt already in progress can finish after that window.
Repetition can duplicate effects. A GET is generally designed to be safe, while a POST may create a second resource unless the API supplies idempotency guarantees. Read the API contract and curl retry behavior before retrying a state-changing request.
A bounded read request with conservative retries
if curl -sS --fail-with-body \
--connect-timeout 5 --max-time 10 \
--retry 2 --retry-delay 1 --retry-max-time 25 \
-o response.json \
https://api.example/items; then
jq . response.json
else
rc=$?
printf 'request failed (curl exit %s)\n' "$rc" >&2
exit "$rc"
fiCommon questions
Frequently asked questions
Why does curl exit 0 after an HTTP 500?
Without a fail option, curl treats receiving a valid HTTP response as a successful transfer. Use --fail or --fail-with-body when detected status 400-and-above responses should produce a nonzero exit, and account for the documented authentication edge cases.
Should scripts use --fail or --fail-with-body?
Use --fail when an error body is unnecessary or should not be written. Use --fail-with-body when the API’s error response is useful, while still handling that body as potentially sensitive and untrusted data.
Is HTTP status 000 a network error code?
No. It is the formatted value curl emits when no HTTP response status is available. Use curl’s nonzero process exit code to identify the transfer failure category.
Keep learning
Sources and next steps
Related mental models
HTTP requests with curl
curl is a command-line data-transfer client that uses a URL to select a protocol and exposes options for constructing requests, inspecting transfers, handling failures, and writing response data. For HTTP, its defaults and options map to request methods, fields, content, redirects, and authentication.
Exit status
An exit status is a small integer produced when a command or pipeline completes; zero conventionally means success and nonzero meanings are defined by that command's interface.
Shell error handling
Shell error handling combines explicit status checks, intentional propagation, validated inputs, cleanup, and selectively enabled shell options rather than relying on exceptions.