521

I was always assuming that when curl got an HTTP 500 response it was returning an exit code that meant failure (!= 0), but that seems to be not the case.

Is there a way I can I make cURL fail with an exitCode different than 0 if the HTTP status code is not 200? I know I can use -w "%{http_code}" but that puts it in STDOUT, not as the exit code (besides, I'm also interested in capturing the output, which I don't want to redirect to a file, but to the screen).

knocte
  • 5,481
  • 2
  • 15
  • 19
  • Anyone getting here long after 2021/02/15: please note that a flag was added to curl that does *exactly* that - see https://superuser.com/a/1626376/25173 below. Of course, make sure your curl version is >= 7.76.0. – noamtm Jan 10 '22 at 10:54
  • It's 2022 now, and (per my example) the latest LTS Jenkins docker includes this. – noamtm Jan 10 '22 at 10:55
  • how is jenkins related to my question? – knocte Jan 10 '22 at 12:44
  • Not related at all, it was an example (showing that the current versions of images tend to include this curl version). – noamtm Jan 10 '22 at 14:13

11 Answers11

525

curl --fail does part of what you want:

from man curl:

-f, --fail

(HTTP) Fail silently (no output at all) on server errors. This is mostly done to better enable scripts etc to better deal with failed attempts. In normal cases when an HTTP server fails to deliver a document, it returns an HTML document stating so (which often also describes why and more). This flag will prevent curl from outputting that and return error 22.

This method is not fail-safe and there are occasions where non-successful response codes will slip through, especially when authentication is involved (response codes 401 and 407).

But it blocks output to the screen.

rampion
  • 5,477
  • 2
  • 18
  • 14
  • 7
    So which parts of it does it do and not do? – rogerdpack Jul 18 '16 at 04:45
  • 11
    @rogerdpack tl;dr it does return nonzero when it detects a bad response, but it wouldn't let OP capture the response – rampion Jul 18 '16 at 07:50
  • 9
    This doesn't catch HTTP 301 Move Permanently. curl still gave exit code 0. – wisbucky May 11 '18 at 22:46
  • 19
    @wisbucky 301 isn't an error, it's a redirection status code. Errors are 4xx and 5xx status codes. – M. Justin Aug 08 '18 at 20:40
  • 4
    @wisbucky to exit nonzero on HTTP error codes *and* handle HTTP redirects correctly use `curl -f -L` and see [this question](https://stackoverflow.com/questions/18474690/is-there-a-way-to-follow-redirects-with-command-line-curl) for details on what `-L` does. – Noah Sussman Apr 24 '19 at 17:49
135

If you just want to display the contents of the curled page, you can do this:

STATUSCODE=$(curl --silent --output /dev/stderr --write-out "%{http_code}" URL)

if test $STATUSCODE -ne 200; then
    # error handling
fi

This writes the page's content to STDERR while writing the HTTP status code to STDOUT, so it can be assigned to the variable STATUSCODE.

Itay Grudev
  • 372
  • 1
  • 3
  • 15
Dennis
  • 48,917
  • 12
  • 130
  • 149
  • 3
    How about if I want to output the response on failure *(non 200)*, but return a non `0` status code from the script? – Justin May 03 '15 at 23:08
  • 2
    @Justin: What about `if [ "$statuscode" -ne 200 ]; then exit "$statuscode"; fi` ? – ghoti Jun 16 '15 at 14:51
  • 6
    @ghoti: Only unsigned 8-bit exit codes are supported, so that could get a little confusing. – Dennis Jun 16 '15 at 16:53
  • 4
    Ah, right -- and codes will wrap at 8 bits, so error 404 becomes exit value 148, 500 becomes 244. Confusing indeed! :-) – ghoti Jun 17 '15 at 20:46
  • 10
    As a slight variation, this captures the code in a variable while redirecting the response to stdout, not stderr: `{ code=$(curl ... as above ...); } 2>&1` The trick is `{ ... } 2>&1` that allows redirecting, while not spawning a different shell as `( ... )` would. – Tobia May 23 '17 at 16:44
  • similarly, you could pipe to `awk` rather than wrapping it in a script: `curl -w '\n%{http_code}' --silent $URL | awk 'END { exit !was200 } { was200=0 ; print prev ; prev = $0 } /^200$/ { was200=1 }'` – rampion Jun 02 '17 at 18:45
  • 1
    Great answer. This seems to be the only way to get both the error exit code and the server response (which might include more details). Only thing I found is that it should accept all 2xx status codes as success, e.g. `if [ $(($STATUSCODE/100)) -ne 2 ]; then exit $STATUSCODE; fi` – Frederik Nov 27 '19 at 10:14
  • I encourage you to use lower case shell variables when you don't intent to `export` them. – ErikE Mar 04 '21 at 17:27
100

Use --fail-with-body. This will cause curl to exit with status code 22 when response code is >=400.

Note though that this flag is brand new (as of 2021/02/15), and was added in version 7.76.0, which postdates @rampion's answer. Thus it might not be available on your system.

https://curl.se/docs/manpage.html

--fail-with-body

(HTTP) Return an error on server errors where the HTTP response code is 400 or greater). In normal cases when an HTTP server fails to deliver a document, it returns an HTML document stating so (which often also describes why and more). This flag will still allow curl to outputting and save that content but also to return error 22.

This is an alternative option to -f, --fail which makes curl fail for the same circumstances but without saving the content.

See also -f, --fail. Added in 7.76.0.

-f, --fail

(HTTP) Fail silently (no output at all) on server errors. This is mostly done to enable scripts etc to better deal with failed attempts. In normal cases when an HTTP server fails to deliver a document, it returns an HTML document stating so (which often also describes why and more). This flag will prevent curl from outputting that and return error 22.

This method is not fail-safe and there are occasions where non-successful response codes will slip through, especially when authentication is involved (response codes 401 and 407).

See also --fail-with-body.

phemmer
  • 2,035
  • 2
  • 15
  • 12
  • 6
    This is really the best answer. I just updated curl on my container with the latest version using a pre-built binary and `--fail-with-body` worked flawlessly. Thank you. – glenbot Nov 23 '21 at 17:52
  • I was pleasantly surprised to find both my laptop and my Jenkins agent already had a new enough version of curl. I realize it's a bit over two years old... but it wouldn't be uncommon for one or the other to be using a version that's 2-5 years old... – ArtOfWarfare Jun 01 '23 at 15:29
99

I was able to do it using a combination of flags:

curl --silent --show-error --fail URL

--silent hides the progress and error
--show-error shows the error message hidden by --silent
--fail returns an exit code > 0 when the request fails

phuclv
  • 26,555
  • 15
  • 113
  • 235
Ricardo Souza
  • 1,151
  • 1
  • 8
  • 9
  • 25
    This doesn't show server reply. I am not OP but I suspect he wanted to see any error message from the server which is returned in body. Besides an `--silent --show-error --fail` works the same as just `-f/--fail`. – waste Oct 20 '17 at 01:12
  • 1
    Actually, `--fail` returns exit code `22`, [as documented](https://ec.haxx.se/usingcurl-returns.html). – Quolonel Questions Nov 10 '17 at 05:03
  • 1
    This doesn't catch HTTP 301 Move Permanently. curl still gave exit code 0. – wisbucky May 11 '18 at 22:46
  • 3
    @wisbucky 301 isn't an error, it's a redirection status code. Errors are 4xx and 5xx status codes. – M. Justin Aug 08 '18 at 20:40
  • 7
    To be fair to @wisbucky, the original question states _"[...] if the HTTP status code is not 200"_. No mention of "error" anywhere prior. – ken Sep 11 '18 at 17:11
  • 2
    @M.Justin According to curl man-page: This method is not fail-safe and there are occasions where non-successful response codes will slip through, especially when authentication is involved (response codes 401 and 407). – youfu Mar 10 '19 at 09:53
  • 1
    @waste It isn't the same as `--fail` alone; `--silent --show-error --fail` gets rid of the progress indicators – binaryfunt Jul 22 '20 at 17:04
76

Most of the answers provided so far will not print the HTTP response body in case an HTTP request fails.

If you would like to print the response body as well, even if the exit code is non-zero due to the HTTP request failing: provided you have curl v7.76 or newer, simply use curl --fail-with-body.

For older versions of curl, try this:

curlf() {
  OUTPUT_FILE=$(mktemp)
  HTTP_CODE=$(curl --silent --output $OUTPUT_FILE --write-out "%{http_code}" "$@")
  if [[ ${HTTP_CODE} -lt 200 || ${HTTP_CODE} -gt 299 ]] ; then
    >&2 cat $OUTPUT_FILE
    return 22
  fi
  cat $OUTPUT_FILE
  rm $OUTPUT_FILE
}
knocte
  • 5,481
  • 2
  • 15
  • 19
jtompl
  • 892
  • 7
  • 5
13

Yes there is a way to do it but is far from obvious as it involves 3 curl options:

curl -s --fail --show-error https://httpbin.org/status/200 > /dev/null
curl -s --fail --show-error https://httpbin.org/status/401 > /dev/null
curl -s --fail --show-error https://httpbin.org/status/404 > /dev/null
curl -s --fail --show-error https://bleah-some-wrong-host > /dev/null

This assures that success (0) happens only when curl end-us with final 2xx return code and that stdout gets the body and that any errors would be displayed to stderr.

Please note that curl documentation may confuse you a little bit because it mentions that --fail could succeed for some 401 codes. Based on tests that is not true, at least not when used with --show-error at the same time.

So far I was unable to find any case where curl will return success when it was not a http-succeds with these options.

sorin
  • 11,660
  • 20
  • 63
  • 73
11

I ended up with this based on Dennis's answer, a quick one-liner that fails for non-200 status codes while retaining the output (to stderr):

[ $(curl ... -o /dev/stderr -w "%{http_code}") -eq 200 ]
shesek
  • 211
  • 2
  • 3
  • Sometimes the best answer is not the accepted one or the one with most votes. This one works cross platform as on macos you do not get the newest curl that has the `--fail-with-body`... same for lots of other platforms. Ugly, but works. – sorin Jan 24 '22 at 10:43
  • This is the best one that works for 301 – Andy Fraley May 05 '23 at 03:58
3

I recently also needed something like this but I also wanted to print the output at all times. I used the http_code extraction and grep to achieve this.

out=$(curl -H"Content-Type: application/json"  -w '\nstatus_code=%{http_code}\n' -s http://localhost:4040${uri} -d "${body}")
code=$(echo ${out}|grep status_code|awk -F"=" '{print $2}')
echo ${out}
if [[ ${code} -ge 400 ]];
then
    exit 2;
fi;
phuclv
  • 26,555
  • 15
  • 113
  • 235
skipy
  • 131
  • 2
3

This is based on @Dennis's solution, but shows the output only on error (return code not 200):

#!/bin/bash

# Create temp file, readable and writable only by current user and root
SCRATCH=$( umask 0077; mktemp -t tmp.XXXXXXXXXX )

# Cleanup temp file on script exit
function cleanup_on_exit {
    rm -f "$SCRATCH"
}
trap cleanup_on_exit EXIT

# Perform curl call
HTTP_CODE=$( curl -s -o "$SCRATCH" -w '%{http_code}' your..stuff..here )

# Analyze HTTP return code
if [ ${HTTP_CODE} -ne 200 ] ; then
    cat "${SCRATCH}"
    exit 1
fi

Alternative HTTP return code handling, where 2XX is OK:

# Analyze HTTP return code
if [ ${HTTP_CODE} -lt 200 ] || [ ${HTTP_CODE} -gt 299 ] ; then
    cat "${SCRATCH}"
    exit 1
fi
t0r0X
  • 209
  • 1
  • 7
  • 1
    Why not use a file descriptor or a fifo instead of a temp file? Seems a little more... unix-y. – ErikE Mar 04 '21 at 17:36
  • 1
    Great suggestion, @ErikE. Why don't you write an answer that shows us how to do it? :) – JakeRobb Mar 31 '21 at 18:39
  • 1
    @Jake I wish I had the time for that! – ErikE Mar 31 '21 at 18:44
  • @ErikE Everything in Unix/Linux/BSD "is a file", even a FIFO. The argumentation (`"more unix-y"`) seems logical first, and I'd like to see such a variant, but without a working example it's just an eyewash. – t0r0X Apr 06 '21 at 09:32
  • @t0r0X Okay then. I just don't like using global temp files in a directory accessible to other processes. Time and again, this creates problems. – ErikE Apr 07 '21 at 23:32
  • I did notice something else. You can be POSIX compliant instead of using bash-isms: `if [ $HTTP_CODE -lt 200] || [ $HTTP_CODE -gt 299 ]; then ... fi`. – ErikE Apr 07 '21 at 23:34
  • @ErikE Thanks for the suggestion, I changed the `if` statements to be POSIX compatible. Sidenote: I tried the past 20+ years to be POSIX compatible, but some time ago I surrendered to the sweetness of BASH and ZSH syntax extensions :-) – t0r0X Apr 14 '21 at 08:46
  • @ErikE I fixed the issue of the temp file being readable by other users, see the `umask 0077`. – t0r0X Apr 14 '21 at 08:54
2

Here was my solution - it uses jq and assumes the body is json

#  this code adds a statusCode field to the json it receives and then jq squeezes them together
# curl 7.76.0 will have curl --fail-with-body and thus eliminate all this
  local result
  result=$(
    curl -sL -w ' { "statusCode": %{http_code}} ' -X POST "${headers[@]}" "${endpoint}" \
      -d "${body}"  "$curl_opts" | jq -ren '[inputs] | add'
  )
#   always output the result
  echo "${result}"
#  jq -e will produce an error code if the expression result is false or null - thus resulting in a
# error return code from this function naturally. This is much preferred rather than assume/hardcode
# the existence of a error object in the body payload
  echo "${result}" | jq -re '.statusCode >= 200 and .statusCode < 300' > /dev/null
Christian Bongiorno
  • 254
  • 1
  • 3
  • 10
1

I recently tried to use the approaches shown here but kept having an error because curl was providing the response_code as 000401 when it should be 401 (curl 7.64.1 (x86_64-apple-darwin19.0). Bash converts 000401 to 257 so trying to test for >=200 and <=299 wasn't reliable.

I end up with this function:

function curl_custom() {
    local RESPONSE_CODE
    RESPONSE_CODE=$(curl -v --silent --output /dev/stderr --write-out "%{response_code}" "$@")

    # Remove leading zeros (if any) to prevent bash interpreting result as octal
    RESPONSE_CODE=${RESPONSE_CODE##+(0)}

    if (( RESPONSE_CODE >= 200 )) && (( RESPONSE_CODE <= 299 )) ; then
        echo "INFO - Got response ${RESPONSE_CODE}"
    else
        echo "ERROR - Request failed with ${RESPONSE_CODE}"
        return 127
    fi
}

Examples on usage:

curl_custom example.com example.com
curl_custom -X PUT -U user:pass file.json example.com/api

Hope it helps.

Gabriel
  • 111
  • 4