How we found a bug in the hyper HTTP library
Quick Summary
Cloudflare Images 팀은 Workers용 Images binding에서 큰 이미지 응답이 200 OK로 보이면서도 잘려 나가는 간헐적 장애를 추적해, hyper의 타이밍 의존적 race condition으로 인한 조기 socket shutdown을 찾아냈습니다.
🖼️ 인포그래픽
🖼️ 4컷 인포그래픽
💡 한 줄 요약
Cloudflare Images 팀은 Workers용 Images binding에서 큰 이미지 응답이 200 OK로 보이면서도 잘려 나가는 간헐적 장애를 추적해, hyper의 타이밍 의존적 race condition으로 인한 조기 socket shutdown을 찾아냈습니다.
📌 핵심 요약
- Cloudflare의 Images 서비스는 Rust와 Workers 기반으로 에지 네트워크 전역에서 동작하며, 클라이언트 연결 처리를 위해 Rust HTTP 라이브러리인 hyper를 사용합니다.
- 2025년 말 Images binding 구조를 FL을 거치는 방식에서 같은 머신의 내부 binding과 Unix socket을 사용하는 더 직접적인 경로로 바꾼 뒤, 큰 이미지 변환 요청에서 응답 본문이 일부만 도착하는 문제가 보고되었습니다.
- 문제는 HTTP 상태가 200이고 서비스 로그에도 오류가 없는데, Content-Length가 약속한 수 MB의 이미지 중 일부만 실제로 전달되는 형태였기 때문에 어느 계층에서 잘림이 발생했는지 파악하기 어려웠습니다.
- 팀은 고객의 중첩 이미지 처리 구성을 재현하고, timeout, hyper 버전, 로컬 재현, Workers runtime, intermediary service 등을 차례로 배제하면서 문제가 Images 서비스의 binding 경로 내부에 있음을 좁혀 갔습니다.
- 결정적 단서는 strace로 syscall을 관찰했을 때 나왔으며, 실패 요청에서는 hyper가 첫 sendto 이후 내부 버퍼에 남은 대량의 이미지 데이터를 보내기 전에 shutdown을 호출해 연결을 끝낸다는 사실이 확인되었습니다.
🧩 주요 포인트
- Cloudflare의 Images 서비스는 Rust와 Workers 기반으로 에지 네트워크 전역에서 동작하며, 클라이언트 연결 처리를 위해 Rust HTTP 라이브러리인 hyper를 사용합니다.
- 2025년 말 Images binding 구조를 FL을 거치는 방식에서 같은 머신의 내부 binding과 Unix socket을 사용하는 더 직접적인 경로로 바꾼 뒤, 큰 이미지 변환 요청에서 응답 본문이 일부만 도착하는 문제가 보고되었습니다.
- 문제는 HTTP 상태가 200이고 서비스 로그에도 오류가 없는데, Content-Length가 약속한 수 MB의 이미지 중 일부만 실제로 전달되는 형태였기 때문에 어느 계층에서 잘림이 발생했는지 파악하기 어려웠습니다.
- 팀은 고객의 중첩 이미지 처리 구성을 재현하고, timeout, hyper 버전, 로컬 재현, Workers runtime, intermediary service 등을 차례로 배제하면서 문제가 Images 서비스의 binding 경로 내부에 있음을 좁혀 갔습니다.
- 결정적 단서는 strace로 syscall을 관찰했을 때 나왔으며, 실패 요청에서는 hyper가 첫 sendto 이후 내부 버퍼에 남은 대량의 이미지 데이터를 보내기 전에 shutdown을 호출해 연결을 끝낸다는 사실이 확인되었습니다.
🧠 상세 정리
1. Cloudflare Images와 hyper 사용 배경
본문은 Cloudflare의 Images 서비스가 Rust로 구현되어 Workers 위에서 실행되며, Cloudflare 에지 네트워크의 모든 머신에서 동작한다고 설명합니다. 이 서비스는 클라이언트 연결을 처리하기 위해 Rust 생태계의 오픈소스 HTTP 라이브러리인 hyper를 사용합니다. 문제의 직접적 배경은 Images binding으로, 개발자가 Workers 안에서 원격 이미지를 더 유연하게 처리할 수 있도록 도입된 기능입니다. 이 binding은 이미지 변환 결과를 반드시 HTTP 응답으로 바로 반환하지 않아도 되게 하고, 여러 최적화 작업을 프로그램 방식으로 조합할 수 있게 해 주는 역할을 합니다.
2. Images binding의 목적과 데이터 흐름
Images binding은 이미지 최적화와 전달을 분리하는 기능으로 설명됩니다. 개발자는 이미지 데이터를 Images API에 직접 전달하고, 크기 변경이나 회전 같은 변환을 적용한 뒤, AVIF 같은 원하는 출력 형식으로 결과를 스트림 형태로 돌려받을 수 있습니다. 이 과정에서 Workers runtime은 socket 연결을 통해 Images 서비스와 통신하고, hyper는 Images 서비스 쪽에서 들어오는 요청을 읽고 응답을 다시 socket에 씁니다. Images 서비스는 변환과 인코딩이 끝난 전체 이미지를 하나의 메모리 블록으로 hyper에 넘기며, 이후 hyper가 내부 버퍼를 socket의 outbound buffer로 flush하는 구조입니다.
3. socket buffer와 flush, shutdown의 정상 동작
본문은 socket 연결을 두 프로세스 사이의 통신 채널로 설명하면서, 양쪽 끝에 운영체제 커널이 관리하는 버퍼가 있다고 설명합니다. 한쪽이 데이터를 쓰면 상대가 읽기 전까지 데이터는 이 임시 공간에 머뭅니다. 상대편 reader가 충분히 빠르면 hyper는 응답 데이터를 한 번에 모두 flush할 수 있고, 모든 데이터가 전송된 뒤 socket에 shutdown을 호출해 더 이상 쓸 데이터가 없음을 알립니다. 반대로 reader가 몇 밀리초만 느려도 outbound buffer가 가득 찰 수 있으며, 이때 hyper는 버퍼에 공간이 생길 때까지 기다린 뒤 남은 데이터를 계속 써야 합니다.
4. FL 경유 구조에서 로컬 Unix socket 구조로의 전환
초기 Images binding은 Workers runtime에서 FL이라는 내부 intermediary service를 거쳐 Images 서비스로 향하는 구조였습니다. FL은 Cloudflare 네트워크의 모든 incoming traffic이 지나가는 내부 서비스로, 보안과 성능 기능을 수행하고 적절한 backend로 요청을 라우팅합니다. 이 구조는 URL interface와 유사해 초기 출시에는 자연스러웠지만, binding 변경이 FL의 release cycle에 묶이는 제약이 생겼습니다. 2025년 12월 Images 팀은 FL 대신 같은 머신에서 실행되는 내부 worker binding을 도입했고, 네트워크 socket 경로와 FL의 DNS lookup, routing 같은 처리 부담을 Unix socket 기반의 직접 연결로 대체했습니다.
5. 200 OK이지만 실제로는 실패한 응답
장애는 rollout 며칠 뒤 고객 보고로 처음 드러났습니다. 고객은 두 단계의 이미지 처리 구조를 사용하고 있었는데, 내부 pipeline은 Images binding으로 R2의 큰 JPEG 배경과 PNG overlay들을 합성해 하나의 JPEG를 만들고, 외부 pipeline은 URL interface로 그 결과를 다시 압축, transcoding, resizing했습니다. 문제는 내부 pipeline의 반환 경로에서 응답 본문이 잘린 채 외부 pipeline으로 전달된 데 있었습니다. 외부 pipeline은 내부 pipeline으로부터 HTTP 200과 수 MB의 Content-Length를 받았지만, 실제 body는 약속된 크기의 일부에 불과했고, 그 결과 상위 계층에서는 message length에 도달하기 전에 EOF가 났다는 오류가 보였습니다.
6. 재현과 계층별 배제 과정
팀은 고객의 중첩 구조를 흉내 내는 worker를 만들고, 이후 레이어를 제거해 binding만으로도 버그를 유발할 수 있는지 확인했습니다. 배치 요청을 날리는 작은 스크립트를 통해 초기 실행에서는 25건 중 19건이 실패했고, 도착한 데이터 크기가 약 200KB로 production socket buffer 크기와 비슷하다는 점도 확인했습니다. 이로써 문제는 고객 설정 자체에 묶인 것이 아니라 재현 가능한 시스템 문제로 좁혀졌습니다. 이후 timeout과 요청 지속 시간의 상관관계, hyper 0.14·1.7·1.8 버전 차이, macOS 및 Debian VM에서의 로컬 통합 테스트, 직접 curl 요청과 captured request replay 등을 시험했지만 결정적 재현은 production 경로에서만 나타났습니다.
7. Workers runtime과 intermediary service를 의심하고 배제한 과정
로컬에서는 실패가 재현되지 않았고, 실제 production concurrency와 Workers runtime client가 socket 반대편에 있을 때만 문제가 나타났기 때문에 팀은 Workers runtime을 의심했습니다. 그러나 runtime이 binding socket을 통해 Images와 통신할 때 사용하는 HTTP client를 조사한 결과, 양쪽 trace 어디에서도 예상치 못한 close나 조기 종료를 나타내는 syscall은 보이지 않았습니다. 같은 client가 다른 여러 서비스에서도 문제없이 사용되고 있다는 점도 runtime 원인설을 약하게 만들었습니다. 이어 distributed tracing으로 잘린 body가 이미 고객 구성의 외부 transformation layer에 도달하기 전부터 존재한다는 점을 확인했고, intermediary service에 instrumentation을 추가해 response forwarding 전 body size를 측정한 결과, 데이터는 Images 서비스를 떠날 때 이미 잘려 있었습니다.
8. strace가 드러낸 조기 shutdown
애플리케이션 레벨의 tracing과 logging만으로는 시스템이 스스로 무엇을 했다고 생각했는지만 볼 수 있었고, 그 관점에서는 요청 처리, 이미지 인코딩, HTTP 200 반환이 모두 정상으로 보였습니다. 팀은 실제 kernel syscall을 확인하기 위해 Images 서비스에 strace를 붙였습니다. strace는 syscall마다 timing overhead를 만들기 때문에 필터를 너무 넓히면 flush와 shutdown 검사 사이의 타이밍이 바뀌어 버그가 사라질 수 있었고, 이 점 자체가 문제가 timing-sensitive하다는 가설을 강화했습니다. 성공 요청에서는 sendto가 여러 번 반복되고 모든 데이터가 나간 뒤 shutdown이 호출됐지만, 실패 요청에서는 첫 sendto로 header와 body 일부만 쓴 직후 shutdown이 호출되어 약 14.9MB 응답 중 약 219KB만 전송되고 나머지는 hyper 내부 버퍼에 남아 있었습니다.
🧾 핵심 주장 / 시사점
- HTTP 200과 정상 로그는 end-to-end 데이터 전달의 성공을 보장하지 않으며, 특히 Content-Length가 큰 스트리밍 응답에서는 실제 전송된 byte 수까지 확인해야 합니다.
- 로컬 재현이 실패하더라도 production의 concurrency, socket buffer, runtime client 조합에서만 발생하는 race condition이 있을 수 있으므로 계층별 배제와 실제 syscall 관찰이 중요합니다.
- 성능 최적화를 위해 네트워크 경로를 로컬 Unix socket 구조로 단순화하면 release 독립성과 속도는 얻을 수 있지만, 기존에는 드러나지 않던 timing-sensitive 버그가 표면화될 수 있습니다.
✅ 액션 아이템
- 2025년 말 FL 경유에서 Unix socket 직접 binding으로 바꾼 구조에서 큰 이미지 변환 응답이 200 응답과 함께 절단되는지를 고정 시나리오로 재현한다.
- 고객 구성 재현 환경에서 timeout, hyper 버전, Workers runtime, 중간 계층 조합을 단계적으로 배제해 장애 위치를 binding 경로 내부로 좁힌다.
- strace로 syscall을 추적해 첫 sendto 이후 내부 버퍼 잔여 데이터 전송 전 shutdown 호출 시점을 정밀 계측해 원인 인과를 확정한다.
❓ 열린 질문
- 어떤 응답 크기 구간에서 Content-Length 대비 실제 전송 바이트 불일치가 가장 먼저 드러나는가?
- hyper가 첫 sendto 직후 잔여 버퍼를 비우지 못한 채 shutdown을 수행하게 만드는 타이밍 조건은 무엇인가?
- FL 경유 경로에서 Unix socket 직접 경로로 전환할 때 어느 binding 단계가 race window를 유발했는가?