Health-check a list of URLs and exit non-zero on failure
Problem statement
After a deploy, a pipeline step must check every endpoint in urls.txt and fail the build if any of them is unhealthy. Write healthcheck.sh <url-file> that skips blank lines and # comments, requests each URL with a 5 second timeout, prints OK or FAIL with the HTTP status for each, prints how many checks failed, and exits 1 if any check failed (0 otherwise). A 2xx status counts as healthy. For the example, assume api and web return 200, legacy returns 503, and old.example.internal does not resolve (curl reports status 000).
urls.txt
# production endpointshttps://api.example.com/healthzhttps://web.example.com/ https://legacy.example.com/statushttps://old.example.internal/Examples
Example 1
Input: `./healthcheck.sh urls.txt; echo "exit: $?"` (under the conditions above)
Output: OK 200 https://api.example.com/healthz
OK 200 https://web.example.com/
FAIL 503 https://legacy.example.com/status
FAIL 000 https://old.example.internal/
2 check(s) failed
exit: 1
Explanation: If every endpoint returned 2xx, the script would print 0 check(s) failed and exit 0.
Hints
Approach
Optimal
${1:?...} aborts with a usage message if no file is given. while IFS= read -r url reads one line at a time without trimming spaces or interpreting backslashes, and || [ -n "$url" ] still processes a last line that has no trailing newline. Blank lines and comments are skipped with continue. curl -s -o /dev/null -w '%{http_code}' --max-time 5 discards the body, prints just the status code, and gives up after 5 seconds; on DNS or connection errors curl prints 000. The regex ^2[0-9][0-9]$ accepts any 2xx. A counter tracks failures, and the final [ "$failed" -eq 0 ] is the last command, so its status (0 or 1) becomes the script's exit code, which is what fails the pipeline.
# Usage: healthcheck.sh <url-file>set -uo pipefail file=${1:?Usage: healthcheck.sh <url-file>}failed=0 while IFS= read -r url || [ -n "$url" ]; do # skip blank lines and comments [[ -z "$url" || "$url" == \#* ]] && continue code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url") if [[ "$code" =~ ^2[0-9][0-9]$ ]]; then echo "OK $code $url" else echo "FAIL $code $url" failed=$((failed + 1)) fidone < "$file" echo "$failed check(s) failed"[ "$failed" -eq 0 ]Follow-up questions
- Check the URLs in parallel so 50 endpoints do not take 50 x 5 seconds. (
xargs -P, background jobs withwait) - Add retries with a delay before declaring an endpoint failed, and follow redirects with
-L.
Frequently asked questions
With set -e, the first failing curl would end the script before the remaining URLs are checked, and a failing [[ ]] test can also trigger an exit in some constructs. A health check should report every endpoint, so failures are counted explicitly and the exit code is set once at the end.
By default curl exits 0 for any HTTP response, including 500, because the transfer itself worked. -f makes it exit 22 on 4xx/5xx, but you lose the status code in the message. Reading %{http_code} covers both cases: 000 for network errors and the real code otherwise.