HTTP status failures can be easy to miss in shell automation because a request that reaches the server, negotiates TLS, and receives a reply can still look successful to the calling script even when the server answered with 404, 401, or 500. Converting those response codes into a hard failure prevents CI gates, health checks, cron jobs, and deployment hooks from continuing with bad upstream state.

By default, cURL reports transport success, not application success, so the HTTP response code has to be checked separately with -w or --write-out if the command runs without an error. The --fail option changes that behavior by treating HTTP status codes of 400 or higher as a failed transfer, while --fail-with-body keeps the same failure behavior but still saves the response payload for debugging or parsing.

The two fail options are mutually exclusive, and --fail-with-body requires cURL 7.76.0 or newer. Authentication challenges, proxies, and some protocol paths can still surface different non-zero cURL exit codes, so shell automation should treat any non-zero exit as failure and log {response_code} when the exact HTTP status must be preserved.

Steps to fail on HTTP errors with cURL:

  1. Run a baseline request against a masked release-manifest URL that returns 404 and print the HTTP status to confirm the default success behavior.
    $ curl --silent -o /dev/null -w "response_code=%{response_code}\n" https://ops-api.example.com/v1/releases/rel-2026-03-29/manifest
    response_code=404
    $ echo $?
    0

    Without --fail or --fail-with-body, the request still exits successfully because the transfer itself completed.

  2. Re-run the same request with --fail so HTTP errors return a non-zero exit and the response body is suppressed.
    $ curl --silent --show-error --fail -o /dev/null -w "response_code=%{response_code}\n" https://ops-api.example.com/v1/releases/rel-2026-03-29/manifest
    curl: (22) The requested URL returned error: 404
    response_code=404
    $ echo $?
    22

    Use --fail when only the success or failure signal matters and the error body is not needed downstream.

  3. Switch to --fail-with-body when the error payload still needs to be saved for inspection or machine parsing.
    $ curl --silent --show-error --fail-with-body -o release-manifest-error.json -w "response_code=%{response_code}\n" https://ops-api.example.com/v1/releases/rel-2026-03-29/manifest
    curl: (22) The requested URL returned error: 404
    response_code=404
    $ echo $?
    22
    $ wc -c release-manifest-error.json
        175 release-manifest-error.json

    --fail-with-body is available in cURL 7.76.0 and newer, and it is usually the better fit when APIs return JSON error details that need to be logged or parsed.

  4. Capture both the HTTP status and the cURL exit value when alerting, retry logic, or wrappers need a stable record of what happened.
    $ curl --silent --show-error --fail-with-body -o release-manifest-error.json -w "response_code=%{response_code}\n" https://ops-api.example.com/v1/releases/rel-2026-03-29/manifest
    curl: (22) The requested URL returned error: 404
    response_code=404
    $ status=$?
    $ printf 'curl_exit=%s\n' "$status"
    curl_exit=22

    The exit status should drive shell control flow, while {response_code} keeps the actual HTTP result visible in logs, metrics, or retry decisions.

  5. Apply the same pattern in automation so jobs stop immediately when an upstream release API returns an HTTP error.
    $ cat check-endpoint.sh
    #!/usr/bin/env bash
    set -euo pipefail
    
    URL="https://ops-api.example.com/v1/releases/rel-2026-03-29/manifest"
    BODY_FILE="release-manifest-error.json"
    
    curl --silent --show-error --fail-with-body \
      -o "$BODY_FILE" \
      -w "response_code=%{response_code}\n" \
      "$URL"
    
    echo "Release manifest is available."

    Point the output file at a scratch or log path, because a failed response can overwrite a previous saved payload with a new error body.