Small thing that probably bites a lot of cron jobs out there: curl through a proxy, wrong password, and $? is 0.
No crash, no stderr. The output file just has an empty 407 in it where the page should be, and whatever reads that file next is going to have a bad day.
I wanted to know exactly when this happens, so I wrote a fake proxy that refuses everyone and pointed curl 8.7.1 at it.
# p407.py - a fake proxy that refuses every login
import http.server, socketserver
class H(http.server.BaseHTTPRequestHandler):
def refuse(self):
self.send_response(407)
self.send_header("Proxy-Authenticate", 'Basic realm="p"')
self.send_header("Content-Length", "0")
self.end_headers()
do_GET = do_CONNECT = do_HEAD = refuse
socketserver.TCPServer(("127.0.0.1", 18407), H).serve_forever()
http:// target: exit 0
curl -s -o out.html -w '%{http_code}\n' -x http://user:pass@127.0.0.1:18407 http://example.com/
echo "exit $?"
407
exit 0
Yep. With a plain http URL curl hands the whole request to the proxy, the proxy says 407, and that comes back like any other response. curl got an answer, job done. Bad password, an @ in the password nobody encoded, plan ran out, doesn't matter, your script sees a clean exit.
-f fixes it
curl -sf -o out.html -x http://user:pass@127.0.0.1:18407 http://example.com/
echo "exit $?"
exit 22
-f tells curl that 400 and up is a failure, so you get 22. --fail-with-body does the same but keeps the body, handy when you want to see what the proxy actually said.
https:// target: fails anyway, different number
curl -sS -o /dev/null -x http://user:pass@127.0.0.1:18407 https://example.com/
echo "exit $?"
curl: (56) CONNECT tunnel failed, response 407
exit 56
Here curl has to ask the proxy for a tunnel first (CONNECT), and the 407 shows up before TLS even starts, so there's nothing to pretend about. 56 on 8.7.1. We got 7 for the same thing on 8.21.0, so if your script matches exact numbers, don't. Non-zero means it broke.
Honestly the https case isn't the scary one. The scary one is the cheap health check or the old scraper hitting an http endpoint, because that's where it fails quietly for days.
The fix I'd use
Check the status code too, not only the exit code:
code=$(curl -s -o out.html -w '%{http_code}' -x "$PROXY" "$URL")
rc=$?
if [ "$rc" -ne 0 ] || [ "$code" -ge 400 ]; then
echo "failed: exit=$rc status=$code" >&2
exit 1
fi
Or just -f plus set -e if you like it short.
Two neighbours of this bug:
- SOCKS5 can't give you a 407 at all, there's no HTTP in between. A bad login there is
curl: (97) User was rejected by the SOCKS5 server. Other number, same problem. -
p@ssin a proxy URL has to bep%40ss. Otherwise curl cuts the URL at the wrong@, sends a username the proxy has never heard of, and you're staring at a 407 for a password you know is right.
We wrote up the rest of curl-through-a-proxy (socks5 vs socks5h and DNS leaks, --noproxy, timing each hop with -w, a five-command check that tells you which layer broke) here: curl proxy: -x, auth, SOCKS5, timing and exit codes.
Disclosure: I work on proxymint, a proxy service. None of the above depends on whose proxy it is.
Top comments (0)