빌드노트
http.client의 다음 응답 조회가 ResponseNotReady로 막힌다
http.client에서 이전 본문을 남기면 다음 request()가 성공해도 getresponse()는 ResponseNotReady로 막힐 수 있다. 고정 길이 응답의 미독·부분·전체 읽기를 비교하고, 작은 본문 소비와 연결 폐기를 다음 요청 전에 결정하는 방법을 설명한다.
Python의 HTTPConnection으로 첫 응답의 상태는 확인했는데, 다음 getresponse()에서 ResponseNotReady('Request-sent')가 발생할 수 있다. 같은 연결을 재사용하면서 첫 응답의 본문을 남겼다면 그 읽기 순서부터 확인한다. 응답 객체를 받는 것과 본문을 끝까지 읽는 것은 다르다. 다음 요청을 보내기 전에 이전 본문 처리를 끝내거나, 그 연결을 닫아야 한다.
자기 코드에서는 두 번째 request() 바로 앞을 본다. 앞선 getresponse()의 반환값을 어느 변수에 보관했는지, 상태나 헤더만 확인한 뒤 넘어가는지, read(몇 바이트) 후 조기에 반환하는 경로가 있는지 확인한다. 같은 변수에 다음 응답을 덮어쓰면 아직 읽어야 할 이전 응답을 놓치기 쉽다. Python 3.12의 getresponse 문서 (새 탭에서 열림)도 새 요청 전에 이전 응답 전체를 읽도록 요구한다.
request가 성공해도 다음 응답을 받을 준비는 안 될 수 있다
별도 비교를 위해 Windows 11·CPython 3.12.10에서 매 조건마다 새 HTTP/1.1 서버를 127.0.0.1의 임의 포트에 열었다. 첫 GET은 Content-Length: 6과 본문 abcdef, 다음 GET은 길이 4와 본문 NEXT를 보냈다. 두 응답은 모두 200이며 연결을 유지했다. 클라이언트 소켓 제한은 1초였다. 실제 서비스의 장애 기록이 아닌 작은 합성 응답 실험이다.
첫 응답 객체를 first에 보관한 뒤 읽는 양만 달리했다. 아래의 성공은 예외 없이 해당 호출이 반환했다는 뜻이다.
첫 본문 처리 다음 request() 다음 getresponse()
읽지 않음 성공 ResponseNotReady('Request-sent')
read(2): b'ab' 성공 ResponseNotReady('Request-sent')
read(): b'abcdef' 성공 200, 본문 b'NEXT'
read(6): b'abcdef' 성공 200, 본문 b'NEXT'
2바이트를 읽은 응답도 6바이트를 전혀 읽지 않은 응답과 같은 예외를 냈다. 일부 데이터가 필요해서 읽었다는 사실만으로 응답 처리가 끝나지는 않았다. 반면 정확한 고정 길이 6바이트를 모두 읽으면 추가로 빈 바이트를 읽는 호출 없이 다음 응답을 받았다.
이때 두 번째 요청은 서버에도 도착했다. 서버 기록에는 같은 TCP 연결에서 첫 경로와 다음 경로가 각각 한 번씩 남았다. getresponse()의 실패를 다음 요청의 미전송으로 해석해서 request()부터 다시 호출하면 안 된다. 실제로 부수 효과가 있는 요청이었다면 재전송 여부를 별도로 판단해야 한다.
CPython 3.12.10 구현 (새 탭에서 열림)은 요청 진행 상태와 이전 응답 객체를 따로 보관한다. getresponse()는 요청을 보냈는지뿐 아니라, 처리 중인 이전 응답이 남아 있는지도 검사한다. 따라서 오류 메시지의 Request-sent와 이전 본문의 미완료 상태가 함께 존재할 수 있다. 이 동작을 이용해 본문보다 다음 요청을 앞세우기보다 문서가 정한 순서를 지킨다.
작은 본문을 소비할 때는 다음 요청보다 앞에 둔다
아래는 기존 코드에서 이전 본문을 마저 읽고 다음 GET으로 넘어가는 부분이다. conn은 timeout=1로 생성한 기존 HTTPConnection, first는 그 연결의 첫 getresponse() 반환값이다. 다음 요청을 아직 보내지 않았고, 첫 응답이 실험처럼 압축·청크 전송 없는 정상적인 6바이트 고정 본문인 경우에 적용했다. next_path는 자신의 다음 조회 경로이며 실험에서는 /next였다.
tail = first.read(6)
print(repr(tail))
conn.request("GET", next_path)
second = conn.getresponse()
print(second.status, repr(second.read()))
미독 상태에서는 b'abcdef', 이미 read(2)를 호출한 상태에서는 남은 b'cdef'를 받았다. 두 경우 모두 다음 출력은 200 b'NEXT'였고 TCP 연결도 유지됐다. read(6)은 새로 6바이트를 더 만들어 읽는 요청이 아니라 최대 6바이트를 읽는 요청이다. 이 고정 길이 응답에서는 남은 4바이트만 반환하고 끝났다. HTTPResponse.read 설명 (새 탭에서 열림)
실제 응답이 6바이트라는 뜻은 아니다. 이 숫자를 모든 응답에 그대로 적용하면 더 긴 본문은 다시 남는다. 작은 고정 길이 응답을 재사용하려면 허용할 전체 크기를 먼저 정하고, 정상적인 메시지 끝까지 소비해야 한다. 이미 일부를 읽었다면 그 양도 예산에 포함한다. 큰 응답을 조각으로 처리하더라도 누적 바이트와 전체 처리 시간에 한도를 둔다. 크기나 종료 시점을 알 수 없거나 더 읽을 필요가 없다면 연결을 닫는 쪽을 선택한다. 예외를 없애려고 임의의 응답에 인자 없는 read()를 추가해 전부 메모리에 올리는 해결은 피한다.
소켓의 timeout도 본문 전체에 대한 시간 예산과 같지 않다. 느리게 데이터가 계속 오는 조건까지 제한하려면 전체 처리 기한이 따로 필요하다. 위 비교는 작은 응답을 즉시 보내는 서버에서 수행했다.
응답과 연결의 상태 표시도 구별한다. 이 버전에서 본문 전체를 읽은 직후 first.closed는 False, first.isclosed()는 True, conn.sock is not None은 True였다. 구현에서 isclosed()는 해당 응답의 읽기 파일이 정리됐는지를 보고, 연결은 이를 이전 응답을 놓아도 되는지 판단하는 데 쓴다. 이 관찰을 TCP 연결이 닫혔다는 뜻으로 해석하거나, closed 하나로 다음 요청 가능 여부를 판정하지 않는다.
본문을 버릴 때는 연결도 닫는다
첫 본문이 필요 없고 더 읽지 않기로 했다면 다음 요청을 보내기 전에 응답과 연결을 함께 정리한다. 다음 코드는 위 소비 코드 대신 넣는 분기다. 두 코드를 연달아 실행하는 순서가 아니다.
first.close()
conn.close()
conn.request("GET", next_path)
second = conn.getresponse()
print(second.status, repr(second.read()))
같은 실험에서 첫 본문을 읽지 않고 이 순서로 처리해도 200 b'NEXT'를 받았다. 서버가 부여한 TCP 연결 번호는 1에서 2로 바뀌었다. Python의 conn 객체는 같아도 기존 TCP 연결을 계속 쓴 결과가 아니다. HTTPConnection의 close·connect 설명 (새 탭에서 열림)대로 연결을 닫았고, 다음 요청에서 다시 연결했다.
first.close()만 실행한 직후에는 응답의 두 닫힘 표시가 모두 True였지만 conn.sock은 여전히 있었다. 응답을 닫는 호출을 연결을 버리는 호출의 대체로 취급하지 않는다. 미독 본문을 포기할 때는 conn.close()까지 실행한다. 전체 작업을 마칠 때도 마지막 응답과 연결을 정리한다.
이미 두 번째 요청을 보낸 뒤의 복구는 위 두 분기의 전제에서 벗어난다. 별도 시도에서 ResponseNotReady 뒤 첫 본문을 읽고 다시 getresponse()를 호출했지만, 다음 응답 대기에서 1초 제한에 걸렸다. 그 시간 초과의 정확한 원인은 확정하지 않았으며, 뒤늦은 읽기를 일반적인 복구 방법으로 권하지 않는다.
또한 ResponseNotReady라는 이름만으로 이전 본문 미독을 확정할 수는 없다. 첫 본문을 다 읽고 새 요청 없이 getresponse()만 다시 호출한 대조 조건에서는 ResponseNotReady('Idle')가 발생했다. 실제 예외가 난 API와 바로 앞의 요청·읽기 순서를 함께 확인해야 한다.
이번에 실행한 범위는 유효한 고정 길이 본문을 보내는 직접 연결의 HTTP/1.1 GET이다. HEAD·204의 정상적인 빈 본문, 청크 전송, 길이 오류·중도 단절, TLS·프록시, 동시 접근과 다른 Python 버전의 결과까지 확인한 것은 아니다. 실험 서버와 모든 소켓은 종료했다.