How to fail on HTTP errors with cURL

By default, curl treats an HTTP reply as a successful transfer as long as it connected, sent the request, and received a response. That means a missing file, broken API route, or expired token can still leave shell automation with exit code 0 unless you explicitly tell curl to fail on HTTP status errors.

Use --fail when any HTTP 4xx or 5xx response should stop the command immediately, and use --fail-with-body when the same failure should still save the response body for debugging or parsing. The upstream curl docs note that --fail is not completely fail-safe around some authentication flows, especially 401 and 407, so log the HTTP status separately when that distinction matters.

The two fail options are mutually exclusive, and --fail-with-body requires curl 7.76.0 or newer. When scripts, CI jobs, or health checks need a stable record of what happened, pair the failure option with --write-out "response_code=%{response_code}\n" so the HTTP status is still visible in logs.

Steps to fail on HTTP errors with cURL:

  1. Start a disposable local HTTP server so the next commands can hit a path that deliberately returns 404.
    $ python3 -m http.server 18081 --bind 127.0.0.1
    Serving HTTP on 127.0.0.1 port 18081 (http://127.0.0.1:18081/) ...

    Leave this process running in its own terminal while you test the next commands. Press Ctrl+C when you are finished.

  2. Request a missing path without a fail option to confirm the default behavior. The server returns 404, but curl still exits successfully because the transfer itself worked.
    $ curl --silent --output /dev/null --write-out "response_code=%{response_code}\n" http://127.0.0.1:18081/missing-release.json
    response_code=404
    $ echo $?
    0

    HTTP status is application-level information. Without --fail or --fail-with-body, curl only reports transport failure.

  3. Add --fail when an HTTP error should stop the command and return error 22.
    $ curl --silent --show-error --fail --output /dev/null http://127.0.0.1:18081/missing-release.json
    curl: (22) The requested URL returned error: 404
    $ echo $?
    22

    Use --fail when you only need a clean success or failure signal and there is no reason to keep the error response body.

  4. Switch to --fail-with-body when the server's error payload still needs to be saved locally.
    $ curl --silent --show-error --fail-with-body --output missing-release.html http://127.0.0.1:18081/missing-release.json
    curl: (22) The requested URL returned error: 404
    $ echo $?
    22
    $ ls missing-release.html
    missing-release.html

    --fail-with-body still returns error 22 for HTTP failures, but it preserves the response body instead of discarding it.

  5. Print the HTTP status into the same log stream when wrappers, CI jobs, or alerts need both the curl exit and the server's response code.
    $ curl --silent --show-error --fail --output /dev/null --write-out "response_code=%{response_code}\n" http://127.0.0.1:18081/missing-release.json 2>&1
    curl: (22) The requested URL returned error: 404
    response_code=404
    $ echo $?
    22

    The curl exit code should drive shell control flow, while {response_code} keeps the exact HTTP result visible in logs and retry decisions.

  6. Reuse the same fail option on a known-good path to confirm that successful HTTP responses still exit cleanly.
    $ curl --silent --show-error --fail --output /dev/null --write-out "response_code=%{response_code}\n" http://127.0.0.1:18081/
    response_code=200
    $ echo $?
    0

    If a script needs to branch on HTTP failures, treat any non-zero curl exit as failure. Do not rely on parsing the response body alone.