빌드노트
HTTP 500인데 curl은 종료 코드 0을 반환했다
HTTP 500 응답을 받아도 기본 curl의 종료 코드는 0일 수 있다. 고정 200·404·500에서 --fail과 --fail-with-body의 종료 코드·본문 파일을 비교하고, PowerShell에서 실패를 반환하는 처리와 정확히 200을 검사할 조건을 설명한다.
curl로 API를 점검했는데 HTTP 500 뒤에도 다음 배포 단계가 실행된다면, 응답 상태와 curl의 종료 코드를 따로 확인해야 한다. 기본 curl은 HTTP 오류 상태만으로 실패하지 않는다. 로컬 서버의 500 응답을 끝까지 받은 실험에서도 종료 코드는 0이었다. 셸이 그 값을 성공으로 받아들이는 것은 이상한 동작이 아니다. curl 8.21.0의 기본 실패 판정 (새 탭에서 열림)
404·500을 실패로 알리면서 오류 본문도 남기려면 --fail-with-body를 쓴다. 본문이 필요 없으면 --fail을 고를 수 있다. 다만 옵션이 반환하는 실패를 셸에서도 처리해야 다음 단계가 멈춘다. 점검 대상이 정확히 200이어야 한다면 상태 비교도 추가한다.
같은 상태를 받아도 종료 코드와 본문 파일은 달랐다
Windows 11, curl 8.21.0에서 IPv4 loopback 서버가 고정 200·404·500을 반환하게 하고 GET 요청을 보냈다. 각 응답 본문은 synthetic status=500처럼 상태를 적은 한 줄이며, 줄바꿈을 포함해 21바이트였다. 실제 API나 배포를 실행한 결과는 아니다.
세 경우 모두 -sS, -o, -w '%{http_code}'와 시간 제한을 같게 두고 실패 옵션만 바꿨다. 출력 파일은 요청마다 새 이름을 사용했다.
실패 옵션 HTTP curl 종료 코드 새 본문 파일
없음 200 0 21바이트
없음 404 0 21바이트
없음 500 0 21바이트
--fail 200 0 21바이트
--fail 404 22 생성되지 않음
--fail 500 22 생성되지 않음
--fail-with-body 200 0 21바이트
--fail-with-body 404 22 21바이트
--fail-with-body 500 22 21바이트
--fail과 --fail-with-body는 HTTP 400 이상을 종료 코드 22로 알린다. 차이는 오류 본문을 출력하느냐다. --fail-with-body에 -o response.txt를 함께 주면 500의 설명도 그 파일에 쓸 수 있다. 이 옵션은 curl 7.76.0부터 지원하며, 두 실패 옵션은 함께 쓰지 않는다. 본문을 남기는 실패 옵션 (새 탭에서 열림)
--fail에서 파일이 없다는 결과는 새 경로를 썼을 때의 관찰이다. 별도로 기존 파일에 PREVIOUS RESPONSE를 넣고 같은 500을 요청하자 예전 내용이 그대로 남았다. 파일이 있다는 사실만으로 이번 응답을 받았다고 판단하면 안 된다. 실패를 조사할 때는 이번 요청의 종료 코드와 출력 파일을 함께 본다.
상태와 종료 코드를 따로 확인한다
다음 비교는 Python 3와 PowerShell 7, curl.exe로 반복할 수 있다. 쓰기 가능한 빈 폴더에서 아래 내용을 status-server.py로 저장하고, 첫 터미널에서 python ./status-server.py로 실행한다. 서버는 이 PC의 127.0.0.1:18765만 사용한다. 포트가 사용 중이면 다른 빈 포트로 서버와 아래 주소를 함께 바꾼다.
from http.server import BaseHTTPRequestHandler, HTTPServer
class Fixed(BaseHTTPRequestHandler):
def do_GET(self):
status = {"/200": 200, "/404": 404, "/500": 500, "/302": 302}.get(self.path, 404)
body = f"synthetic status={status}\n".encode("ascii")
self.send_response(status)
if status == 302:
self.send_header("Location", "/200")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 18765), Fixed)
print("http://127.0.0.1:18765", flush=True)
server.serve_forever()
두 번째 PowerShell 터미널을 같은 폴더에서 열고 아래를 실행한다. response.txt는 덮어쓰므로 기존 자료가 없는 폴더를 쓴다. 자신의 API를 점검하려면 $url만 직접 관리하는 읽기 전용 GET 주소로 바꾼다. 로컬 실습의 --noproxy '*'는 프록시를 거치지 않게 하는 옵션이므로, 프록시가 필요한 API에서는 연결 환경에 맞게 조정한다. 프록시 제외 설정 (새 탭에서 열림)
$url = 'http://127.0.0.1:18765/500'
$http = & curl.exe -q -sS --noproxy '*' --connect-timeout 1 --max-time 3 --fail-with-body -o response.txt -w '%{http_code}' $url
$rc = $LASTEXITCODE
"http=$http exit=$rc"
Get-Content -Raw ./response.txt
실행 환경은 PowerShell 7.6.6과 Python 3.12.10이었다. 500 요청의 표준 출력은 다음과 같았고, 표준 오류에는 curl: (22) The requested URL returned error: 500이 나왔다. 두 스트림을 함께 보는 터미널에서는 표시 순서가 달라질 수 있다.
http=500 exit=22
synthetic status=500
같은 명령에서 --fail-with-body를 지우면 http=500 exit=0이었다. /200과 /404로 바꾸고 실패 옵션을 달리하면 앞 비교의 상태와 종료 코드를 확인할 수 있다. --fail로 404·500을 요청하면 새 본문 파일을 만들지 않았으므로, 마지막 Get-Content의 파일 없음 오류를 curl 오류와 혼동하지 않는다. 비교마다 별도 빈 폴더를 쓰면 이전 본문이 남는 혼란을 피할 수 있다.
-q는 첫 옵션일 때 기본 curl 설정 파일을 읽지 않게 한다. 여기서는 숨은 설정의 영향을 줄이려고 붙였다. 연결 제한 1초와 전체 전송 제한 3초는 실습에서 기다림을 제한하는 값이며, HTTP 500을 실패로 바꾸는 옵션은 아니다. 기본 설정 제외 (새 탭에서 열림), 전송 시간 제한 (새 탭에서 열림)
-sS도 실패 기준을 바꾸지 않는다. -s는 진행 표시와 오류 메시지를 숨기고, -S는 실패할 때 오류 메시지를 다시 보이게 한다. 실제 500 비교에서 기본 curl에 -sS만 주면 종료 코드 0에 오류 메시지도 없었다. --fail-with-body -s는 메시지 없이 22, --fail-with-body -sS는 오류 메시지와 함께 22였다. 조용한 출력 (새 탭에서 열림), 오류 표시 (새 탭에서 열림)
-w '%{http_code}'는 전송 뒤 상태를 표준 출력에 적는다. 본문은 -o로 파일에 보내 두었으므로 $http에는 상태만 들어간다. 500이라는 문자열을 출력했다고 종료 코드가 바뀌지는 않았다. -w를 빼도 실패 옵션의 22는 유지됐다. write-out과 http_code 정의 (새 탭에서 열림)
다음 단계 앞에서 실패를 반환한다
PowerShell 기본 설정에서 curl 다음 줄에 'NEXT_STEP'을 출력하도록 두자, 실패 옵션 유무와 관계없이 그 줄이 실행됐다. pwsh -NoProfile -File로 실행한 스크립트의 최종 종료 코드도 0이었다. curl이 22를 반환해도 이후 명령을 그대로 실행하는 스크립트라면 배포를 멈추지 못한다.
다음 내용을 check.ps1로 저장한다. curl 직후 $LASTEXITCODE를 저장하고 실패하면 그 값으로 스크립트를 끝낸다. 그다음 상태가 200인지 검사한다. NEXT_STEP은 다음 배포 단계를 대신하는 출력일 뿐이다. PowerShell의 LASTEXITCODE와 스크립트 종료 (새 탭에서 열림)
param([Parameter(Mandatory)][string]$Url)
$http = & curl.exe -q -sS --noproxy '*' --connect-timeout 1 --max-time 3 --fail-with-body -o response.txt -w '%{http_code}' $Url
$rc = $LASTEXITCODE
if ($rc -ne 0) {
[Console]::Error.WriteLine("curl failed: exit=$rc HTTP=$http; inspect response.txt if present")
exit $rc
}
if ($http -ne '200') {
[Console]::Error.WriteLine("unexpected HTTP=$http; expected 200")
exit 1
}
'NEXT_STEP'
같은 폴더에서 pwsh -NoProfile -File ./check.ps1 -Url 'http://127.0.0.1:18765/500'으로 실행하고, 호출한 PowerShell에서 바로 $LASTEXITCODE를 읽는다. 이 경우에는 22이며 NEXT_STEP은 출력되지 않는다. /200이면 0과 NEXT_STEP, /404이면 22와 다음 출력 없음이었다. 실패 본문은 response.txt에 남았다.
마지막 상태 비교가 필요한 이유는 302 대조군에서 보였다. 로컬 /302는 /200을 가리키는 Location을 보냈다. 리다이렉트를 따라가는 옵션을 주지 않았기 때문에 두 실패 옵션 모두 HTTP 302, 종료 코드 0이었다. 위 스크립트는 이를 unexpected HTTP=302; expected 200으로 알리고 1로 끝냈다. 상태 비교를 제거한 버전에서는 NEXT_STEP이 실행됐다.
따라서 400 이상만 거절하면 되는 점검은 curl 종료 코드 처리까지 적용하고, 정확히 200을 요구하는 점검은 마지막 비교도 유지한다. 204 등을 허용해야 한다면 API 계약에 맞는 허용 상태를 정한다. 오류 본문이 필요 없을 때는 --fail-with-body를 --fail로 바꿀 수 있지만, 오류 본문을 보존한다는 보장은 사라진다.
시간 초과도 같은 실패 분기에서 멈춰야 한다. 별도의 로컬 대조에서는 헤더 전송을 늦추고 --max-time 0.2를 주자 종료 코드 28과 http_code=000이 나왔다. 000은 서버가 보낸 HTTP 상태가 아니다. 22는 이 실험의 HTTP 오류 판정이고, 28은 시간 초과이므로 대응할 원인이 다르다. 상태만 검사하지 말고 curl 종료 코드를 먼저 확인한다. curl 종료 코드 (새 탭에서 열림)
실습을 마치면 첫 터미널에서 Ctrl+C로 서버를 종료한다. 직접 확인한 범위는 고정 200·404·500·302, 헤더 수신 전 시간 초과와 기본 설정의 PowerShell 7이다. 인증 협상, 프록시·TLS, 리다이렉트 추적, 여러 URL을 한 번에 요청하는 경우와 다른 셸의 중단 정책은 검증하지 않았다. 공식 --fail 문서도 인증 관련 401·407에서는 실패 상태가 빠져나갈 수 있다고 설명한다. 인증이 포함된 점검에서 이 결과를 모든 경우에 대한 보장으로 확대하지 않는다.