빌드노트
curl이 두 주소를 오가다 리다이렉트 오류로 끝난다
curl의 리다이렉트 오류 47은 이동 상한 도달을 뜻한다. 두 경로 순환과 정상 유한 체인의 실제 요청 로그·마지막 URL·이동 횟수를 비교하고, 상한을 늘리기 전에 되돌리는 Location을 만든 설정으로 조사 범위를 좁힌다.
curl -L로 페이지를 요청했는데 Maximum (3) redirects followed와 종료 코드 47이 나왔다면, 먼저 각 응답의 Location과 그 뒤에 실제로 요청한 URL을 이어 본다. 이 오류가 알려 주는 것은 이동 횟수가 상한에 도달했다는 사실이다. 순환인지, 끝이 있는 체인에 비해 상한이 낮은지는 이동 기록으로 구분해야 한다.
curl 공식 매뉴얼의 --max-redirs (새 탭에서 열림)는 자동으로 따라갈 이동 횟수를 제한하며, 기본 상한은 50이다. 종료 코드 47 (새 탭에서 열림)도 이 상한 도달을 뜻한다. 상한을 키우기 전에 같은 두 주소가 서로를 목적지로 돌려보내는지 확인하면, 조사할 설정의 범위를 좁힐 수 있다.
GET 요청의 응답과 다음 목적지를 함께 본다
다음 명령은 Windows PowerShell과 curl.exe 기준이다. $target을 자신이 확인할 실제 GET 주소로 바꾼다. 아래 비교와 같은 조건으로 보려면 쿠키·인증 없이 조회할 수 있는 주소를 쓴다. 명령은 본문을 버리고 상세 로그, 마지막 요청 URL, 따라간 이동 횟수, 마지막 HTTP 상태와 curl 종료 코드를 남긴다.
$target = 'https://example.com/replace-with-your-path'
curl.exe -q -sS -L --max-redirs 3 --max-time 5 -v -o NUL -w 'last_url=%{url_effective}\nredirects=%{num_redirects}\nhttp=%{http_code}\n' $target
"exit=$LASTEXITCODE"
-L은 응답이 가리키는 주소로 이동하게 한다. --max-redirs 3은 이동을 세 번까지 따라가게 하고, --max-time 5는 이번 전송의 시간도 제한한다. 첫 옵션인 -q는 기본 curl 설정 파일을 읽지 않게 한다. 따라서 그 파일에 의존하는 요청과는 조건이 달라질 수 있다. Location 옵션 (새 탭에서 열림), 시간 제한 (새 탭에서 열림), 기본 설정 파일 제외 (새 탭에서 열림)
-v의 요청 줄과 응답 상태·Location을 읽는다. 호스트가 바뀐다면 경로만 보지 말고 상세 로그의 전체 목적지 URL과 Host도 함께 확인한다. last_url은 성공한 최종 페이지라는 뜻이 아니라 curl이 마지막으로 가져온 URL이다. redirects는 최초 요청을 제외하고 실제 따라간 이동 횟수다. 마지막 응답에 적힌 Location을 아직 요청하지 못한 채 끝날 수 있다. write-out 변수 정의 (새 탭에서 열림)
세 번 이동했지만 요청은 네 번이었다
직접 비교할 때는 127.0.0.1에만 열린 합성 HTTP 서버를 만들었다. /loop/a는 항상 302와 Location: /loop/b를, /loop/b는 항상 302와 Location: /loop/a를 보냈다. Windows 11, Python 3.12.10, PowerShell 7.6.6, curl 8.21.0에서 실행했다. 실제 사이트 장애 기록이 아니라 두 응답이 서로 되돌리는 조건을 만든 실험이다.
위 PowerShell 명령의 대상만 http://127.0.0.1:17310/loop/a로 바꿔 실행했다. 아래는 상세 로그에서 요청 줄·상태·Location을 추린 것과 출력값이다. 포트 17310은 이 실행에서만 사용한 임시 포트다.
> GET /loop/a HTTP/1.1
< HTTP/1.1 302 Found
< Location: /loop/b
> GET /loop/b HTTP/1.1
< HTTP/1.1 302 Found
< Location: /loop/a
> GET /loop/a HTTP/1.1
< HTTP/1.1 302 Found
< Location: /loop/b
> GET /loop/b HTTP/1.1
< HTTP/1.1 302 Found
< Location: /loop/a
curl: (47) Maximum (3) redirects followed
last_url=http://127.0.0.1:17310/loop/b
redirects=3
http=302
exit=47
서버가 받은 경로도 /loop/a → /loop/b → /loop/a → /loop/b였다. 최초 요청 한 번에 이동 세 번이 더해져 요청은 네 번이었다. 마지막 응답은 다시 /loop/a를 가리켰지만, 네 번째 이동은 허용되지 않았다. 따라서 last_url은 /loop/b로 남았다. http=302는 마지막 서버 응답이고 exit=47은 curl 프로세스의 실패 코드다.
이 실험에서는 Location이 /로 시작하므로 같은 scheme·호스트·포트에 해당 경로를 붙인 주소로 이동했다. 일반적인 상대 Location은 그 응답을 받은 요청 URL을 기준으로 해석한다. 헤더 문자열만 나열하지 말고 해석된 전체 URL과 실제 다음 요청을 연결해야 한다. RFC 9110의 Location 정의 (새 탭에서 열림)
같은 서버에서 상한만 6으로 올려도 종료 코드는 47이었다. 서버에는 일곱 요청이 도착했고, 경로는 계속 두 주소를 번갈아 방문했으며 마지막은 /loop/a였다. 이 고정 응답 조건에서는 상한 증가가 반복 횟수만 늘렸다. 반대로 -L을 빼면 첫 /loop/a의 302에서 종료 코드 0으로 끝났지만, 목적지 페이지에 도달한 것은 아니었다.
같은 오류가 나는 유한 체인도 비교했다
정상 비교는 /chain/start → /chain/middle → /chain/done으로 만들었다. 앞의 두 경로는 각각 다음 경로로 302를 보내고, 마지막 경로는 200을 반환했다. 같은 curl 옵션에서 이동 상한만 1과 2로 바꿨다. 다음은 마지막 URL에서 공통 주소 http://127.0.0.1:17310을 생략해 정리한 실제 결과다.
상한 1: last_url=/chain/middle redirects=1 http=302 exit=47
상한 2: last_url=/chain/done redirects=2 http=200 exit=0
상한 1의 서버 로그에는 /chain/start, /chain/middle만 있었다. 상한 2에서는 /chain/done까지 도착했다. 47이라는 코드 하나만으로 순환을 확정할 수 없는 이유다. 끝이 있는 체인을 확인했고 필요한 이동 수가 알려져 있다면 그에 맞는 유한한 상한을 정할 수 있다. 이 예에서 2로 올린 판단을, 두 주소를 계속 오가는 요청에 그대로 적용할 수는 없다.
결과가 다르면 마지막 응답부터 다시 읽는다. 시간 초과나 연결 실패라면 이 실험의 상한 도달과 다른 실패다. 3xx는 계속 나오지만 아직 같은 URL로 돌아오지 않았다면, 현재 기록만으로 순환 여부를 확정할 수 없다. exit=0이어도 마지막 HTTP 상태와 URL이 원래 얻으려던 응답인지 함께 확인한다.
되돌리는 응답을 만든 규칙부터 좁힌다
자기 서비스에서는 처음 되돌아온 URL을 찾고, 그 직전 요청·응답 쌍을 따로 표시한다. 비교할 대상은 경로 이름만이 아니라 scheme, 호스트, 포트, 경로와 쿼리를 포함한 전체 URL이다. 다음은 관찰된 주소 모양에서 정할 수 있는 조사 후보이며, 이번 합성 서버에서 실제 프록시나 프레임워크 설정을 검증한 결과는 아니다.
- 같은 호스트에서 HTTP와 HTTPS가 번갈아 나온다면, 양쪽 scheme으로 보내는 이동 규칙과 프록시 뒤 애플리케이션이 원래 요청 scheme을 판단하는 설정을 확인한다.
www가 있는 호스트와 없는 호스트를 오간다면, 각 호스트의 대표 주소 지정 규칙이 서로 반대인지 확인한다.- 같은 호스트의
/docs와/docs/를 오간다면, 끝 슬래시를 붙이는 규칙과 없애는 규칙이 동시에 적용되는지 확인한다.
주소 모양만으로 Location을 만든 계층까지 알 수는 없다. 해당 요청을 처리한 프록시·애플리케이션 로그와 이동 규칙을 대조해, 되돌리는 응답을 만든 곳을 찾는다. 수정 후에는 같은 GET과 제한값으로 다시 관찰해, 의도한 주소와 HTTP 상태에 도달하고 역방향 이동이 사라졌는지 확인한다.
이번에 직접 확인한 범위는 쿠키·인증 없는 HTTP GET, 고정된 두 경로 순환과 두 번 이동하는 유한 체인이다. 모든 요청은 loopback에만 보냈고 서버도 종료했다. 실제 CDN, HTTPS 프록시, 로그인 상태에 따라 바뀌는 체인은 실행하지 않았다. 상태가 변하는 흐름에서는 같은 URL 재방문만으로 영구 순환을 단정할 수 없으며, 이 실험만으로 특정 사이트의 설정 원인을 확정할 수도 없다.