curl Exit Code Zero Doesn’t Mean Your HTTP Check Passed
A curl command can finish successfully while the endpoint returns an error. Build a Bash check that verifies the expected HTTP status and retains the response body.
DM
Imagine a deployment script checking an application’s old readiness URL. The server returns a perfectly valid 404 page, curl finishes, and the shell step goes green. The request reached a server; the check still failed its purpose. A useful curl HTTP status check must inspect both the command’s outcome and the response your endpoint promises.
That deployment is hypothetical, but the behavior is documented. The curl FAQ explains why receiving an HTTP error response can still count as a successful transfer. Exit code zero alone doesn’t tell your script that the application is ready.
The first improvement is --fail-with-body. For an HTTP status of 400 or higher, it makes curl report an error while retaining the body; --fail suppresses that body instead. The curl manual documents exit code 22 and notes that --fail-with-body was added in 7.76.0. Check the installed version before using it.
But adding that option doesn’t turn the command into an exact readiness test. Suppose your route promises 200, yet now redirects to a login page. A response below 400 can pass the HTTP failure threshold while violating your contract. Decide which status is acceptable before writing the condition. A route designed to return 204 needs a different expectation from the example below.
Here’s a Bash example for a read-only endpoint whose documented success response is exactly 200. Save it as probe.sh, then pass your endpoint URL with bash probe.sh https://your-host.example/ready. It makes one request, keeps the response body out of standard output, and reports the directory where the body can be inspected.
#!/usr/bin/env bash
# Pass a read-only endpoint whose documented success status is exactly 200.
url=${1:?Usage: probe.sh URL}
probe_dir=$(mktemp -d) || exit 1
printf 'Response body directory: %s\n' "$probe_dir" >&2
if http_code=$(curl -q --silent --show-error --fail-with-body \
--connect-timeout 3 --max-time 10 \
--output "$probe_dir/body" \
--write-out '%{response_code}' \
"$url"); then
if [ "$http_code" = "200" ]; then
printf 'Expected HTTP 200 received.\n'
else
printf 'Unexpected HTTP status: %s\n' "$http_code" >&2
exit 1
fi
else
curl_status=$?
printf 'curl failed: exit=%s HTTP=%s\n' \
"$curl_status" "$http_code" >&2
exit "$curl_status"
fi
The body and HTTP status have separate destinations. curl’s write-out documentation describes %{response_code} and explains that write-out uses standard output by default. Here, --output keeps the body in a file, so the command substitution captures the status without mixing in HTML or JSON.
The first if asks whether curl completed successfully under the selected options. The second asks whether the HTTP status is the one this endpoint promises. In the failure branch, curl_status=$? captures the command’s status before a later command replaces it. Bash’s conditional constructs reference documents how zero and nonzero statuses select the branches. Keep this as a separate script so its exit stops the check, and make the caller honor that failure.
The leading -q disables curl’s default configuration-file loading. --silent --show-error removes the progress display while preserving error messages. These are documented in the curl manual. Environment variables and the network you run from still matter; this isn’t a hermetic network test.
The time limits also serve different purposes. Everything curl’s timeout guide distinguishes connection setup from the whole transfer attempt. The example allows three seconds to connect and ten seconds for the attempt. Those are illustrative budgets, not a service-level recommendation. Choose limits that fit your endpoint and deployment deadline.
Redirects are deliberately not followed here. For the hypothetical retired readiness route, an unexpected redirect should stop the check so you can inspect the destination and fix the URL or routing rule. If redirects are part of your intended design, define the permitted destination and final response explicitly rather than treating any reachable landing page as evidence of readiness.
This example was exercised against a local HTTP fixture during article preparation, using Bash 5.2.37 and curl 8.14.1. A 200 response produced exit 0; 204 and 302 produced exit 1; 404 and 503 produced exit 22. The error bodies for 404 and 503 were retained. These are controlled branch checks, not a production deployment test, TLS test, authentication test or performance benchmark.
The script deliberately says “Expected HTTP 200 received,” rather than “Application healthy.” A server can return 200 with the wrong page or a JSON error object. If readiness requires a particular response field or a dependency check, validate that documented contract after the status check using an appropriate parser. Don’t substitute a loose text search that might match an error message.
Treat the saved body as diagnostic material, not an artifact ready for the next deployment stage. A failed transfer may leave partial data or no body file. The temporary directory is retained by this example; inspect it with suitable access, redact anything sensitive before sharing, and remove it after diagnosis. A recurring probe needs a deliberate retention policy so repeated failures don’t accumulate files indefinitely.
If the status is 502, stop at that observation before guessing which server failed. The NGINX request-path guide follows the public endpoint, proxy and application separately. A better curl condition can expose a failed check; it doesn’t identify the failing hop on its own.
For an application you operate on Cube, run the check from the network location that matters to the decision. A successful loopback request inside the guest doesn’t establish that a public hostname reaches the same service. Cube provides a Linux microVM with root access; you remain responsible for the application’s readiness contract and deployment logic.
Pick one existing deployment check and write down its expected status and body contract. Try a known successful response, a wrong path and an unexpected redirect in an environment you control. Confirm that the calling job actually stops on failure. The green step becomes useful when you can explain exactly what it verified.
Creation context: this AI-assisted article was assembled from the linked primary documentation. Its deployment story is hypothetical. Only the local fixture checks described above were run; the cover is a generated conceptual illustration.
Run your application on Cube
Explore Cube’s Linux microVMs, then build deployment checks around your application’s actual response contract.




