새소식

Welcome to the tech blog of Junior Backend Developer Myoungji Kim!

Backend

[Apache] mod_proxy_fcgi의 chunked 전송 처리 버그 디버깅 기록

  • -

🚨어느 날, 콜백 API에서 오래전부터 이상한 문제가 있다는 이야기를 들었다.

요청값을 제대로 보냈는데도 간헐적으로 400 에러가 발생한다는 내용이었다.

 

그리고 로그를 확인해보니 아래와 같았다.

[ERROR] mode 값이 없습니다.

 

처음에는 요청 측에서 데이터를 보내지 않은 것처럼 보였지만,

담당 부서와 함께 확인해보니 동일한 CURL 요청을 보내도 간헐적으로 에러가 발생하고 있었다. 🧐

curl -X POST 'http://{domain}/{api_path}.php
    --header 'Content-Type: multipart/form-data'
    --form 'mode={mode}'
    --form 'data={data}

 

문제는 Content-Type이 multipart/form-data인 경우에만 form 데이터가 일부 유실되는 현상이 재현되고 있었고,

히스토리를 확인해보니 이 버그는 2-3년 가까이 묵혀 있던 문제였다.

 

사실, 파일 전송이 아닌 상황에서 multipart/form-data를 쓸 이유가 없기도 했고,

Content-Type을 다른 값으로 바꾸면 정상적으로 동작했었다.

 

다만 단순히 “그냥 바꾸자”로 끝내기에는 우리 서비스는 multipart/form-data로 요청 보내면 안 된다로 마무리 짓는 느낌이었다..
그래서 찜찜함을 남기기보다는, 근본 원인을 찾아서 해결해야겠다고 결심했다. 💪

 

결론부터 말하자면,

문제의 원인은 Apache + mod_proxy_fcgi + PHP-FPM 조합에서 chunked 전송을 제대로 처리하지 못한 것이었다.

 

그리고 지금부터 쓸 내용은, 이 결론에 도달하기까지 수없이 디버깅했던 여정이다 ✨


📌 분석 시작

1️⃣ 솔루션 로직 내 Exception 발생 구간 확인

솔루션 로직에서 request를 조회하는 구간에는 특별한 문제점이 없었고, 로그에도 빈 값으로 기록되어 있었다.

 

2️⃣ 패킷 확인

 “서버까지 데이터가 정상적으로 도달하지 못했다면?”을 의심하며 tcpdump/tshark 로 패킷을 캡쳐해서 확인해보았다.

그러나, 실패 처리된 요청들 모두 패킷 안에 form data 값이 정상적으로 포함되어 있었다ㅠ

[root@server /]# tshark -r output.pcap -Y http.request -T fields -e http.file_data 
Running as user "root" and group "root". This could be dangerous. 

--dThJUESdO6IRH2RWnmkObgXC7Tpu-UfaocvrmZ 
Content-Disposition: form-data; name="mode" 
Content-Type: text/plain;charset=UTF-8 
Content-Length: 3

ssl
--dThJUESdO6IRH2RWnmkObgXC7Tpu-UfaocvrmZ
 

📌 즉, 클라이언트 → 아파치 서버 구간에서는 데이터가 유실되지 않았고,

문제는 아파치 이후(PHP-FPM, 애플리케이션)에서 발생했다.

 

3️⃣ PHP 에러 로그 확인

⚠️ 혹시 PHP 설정 문제일까 의심하여 설정값들을 확인해보았다.

  • phpinfo() 확인 → post_max_size: 25M, max_input_vars: 100,000 모두 충분
  • PHP 에러 로그 확인 → /usr/local/php/logs/, /usr/local/log/php-fpm.error.log 등 관련 에러 없었음

📌 결론: PHP 설정 부족이나 명시적 에러는 아니다.

이를 통해, PHP 진입 이전(웹서버 → PHP-FPM 사이)에서 문제가 발생한 것이 확실해졌다.

 

4️⃣ 아파치 Access 로그 확인

검색해보니 Apache + mod_proxy_fcgi + PHP-FPM 환경에서는, Keep-Alive 연결에서 한 TCP 연결을 동시에 들어온 HTTP 요청에 재사용할 경우, PHP-FPM 입장에서는 $_POST가 비어있거나 일부 필드만 존재할 수 있다는 글들이 있었다.

그래서 Keep-Alive 를 확인해보기 위해 아파치 로그 포맷에 status를 추가해보았다.

# AS-IS
LogFormat "%h %l %u %t \"%r\" %>s %b ..."

# TO-BE
LogFormat "%h %l %u %t \"%r\" %>s %b ... conn=%{connection}C reqno=%k status=%X reqid=%{UNIQUE_ID}e"

 

참고로, status 값에 대한 의미는 아래와 같다. 

만약 keep-alive 이슈가 맞다면 status 값에 +로 확인이 되어야 한다.

X : connection aborted before response completed
+ : connection still open, request keep-alive 상태
- : connection closed at response completion (정상 종료)

 

다만, 실패 케이스 요청에 대해서도 status 값이 - (정상 종료)로 기록되었다.

10.17.0.121 - - [17/Sep/2025:19:46:13 +0900] "POST /{api_url} HTTP/1.1" 200 15 "-" "-" "ReactorNetty/1.1.13" - - - exec=0 /{api_url} "cache invalidated by POST" conn=- reqno=0 status=- reqid=-

 

📌즉, Keep-Alive 연결로 인한 POST 데이터 유실 가능성은 로그상에서 확인되지 않아 Keep-Alive 경합 이슈도 아니었다.

 

5️⃣ Java WebClient 요청 방식 확인

⚠️ “혹시 Java WebClient에서 문제가 발생하는 건 아닐까?”도 의심했다.

이유는 단순했다. 내가 서버에서 직접 Curl을 날렸을 때는 문제 없이 동작했는데, 특정 요청지(Java WebClient)에서만 body 값이 유실되고 있었기 때문이다.

실제로 이슈가 발생한 요청 건들의 웹로그를 확인해보니 모두 User-Agent가 "ReactorNetty/1.1.13" 였다.

 

그래서 Curl과 Java WebClient 요청을 각각 비교해보았다.

 

Curl 요청 시 로그:

REQID=68cc98409b2e0
METHOD=POST
HEADER={"Content-Type":"multipart/form-data; boundary=..."}
CONTENT_LENGTH=265
POST={"mode":"ssl","key":"my-secret-key"}

 

Java WebClient 요청 시 로그:

REQID=68cc9859925d4
METHOD=POST
HEADER={"Content-Type":"multipart/form-data", "Connection":"close", "Transfer-Encoding":"chunked"}
CONTENT_LENGTH=0
POST=[]

 

📌 지금까지 확인된 사실:

  1. tcpdump로 보면 요청 바디(TCP 패킷)에는 데이터가 정상적으로 존재.
  2. 하지만 Apache → PHP-FPM 구간에서, PHP 입장에서는 body가 비어있음.
  3. 결과적으로 $_POST / php://input이 빈 값으로 기록됨.

즉, 요청지에서 보냈을 때 네트워크 레이어에는 데이터가 존재하지만, 애플리케이션 레이어에서는 사라지고 있었다.

 

6️⃣ mod_dumpio를 활용한 proxy_fcgi 로그 모니터링

php request header 값을 통해 chunked 된 데이터로 넘어온 점까지는 확인했고, 최종적으로 proxy_fcgi의 상세 동작까지 확인하기 위해 dumpio 모듈을 활성화해보았다.

 

현재 테스트 서버의 Apache LogLevel은 warn으로 설정되어 있기에, 보다 상세한 로그를 위해 dumpio 모듈을 켜고 로그 레벨을 올렸다.

LoadModule dumpio_module modules/mod_dumpio.so

DumpIOInput On
DumpIOOutput On
LogLevel dumpio:trace7 proxy:trace8 proxy_fcgi:trace8

 

이후 실패 케이스 테스트 시, proxy_fcgi 로그에 다음과 같은 라인이 찍혔다.

[Fri Sep 19 13:39:54.466445 2025] [proxy_fcgi:trace8] [pid 1605:tid 2874] 
mod_proxy_fcgi.c(413): [client 10.17.0.42:37433] 
AH01062: sending env var 'HTTP_TRANSFER_ENCODING' value 'chunked'

 

📌 의미: Apache가 Transfer-Encoding: chunked 헤더를 그대로 FPM에 전달했다는 뜻.

 

원래 기대 동작은 아래와 같다.

 

  • HTTP/1.1 요청에서 Transfer-Encoding: chunked가 들어오면
  • Apache가 chunked body를 디코딩 → Content-Length 계산
  • PHP-FPM에 전달, HTTP_TRANSFER_ENCODING 제거
  • PHP-FPM은 항상 CONTENT_LENGTH=N 형태로 받아야 정상 처리

 

하지만 이번 상황은!

  • HTTP_TRANSFER_ENCODING=chunked 그대로 전달
  • 동시에 CONTENT_LENGTH=0 → FPM 입장에서는 "바디 없음" 처리
  • tcpdump에서는 body 존재하지만 PHP-FPM은 읽을 근거가 없어 무시

✅ 결론: Apache mod_proxy_fcgi가 chunked decoding에 실패 → PHP-FPM 전달 꼬임

 

7️⃣ 아파치 환경 확인

열심히 뒤적뒤적한 결과.. 공식 문서에서 Apache 2.4.47 릴리즈에서 관련 문제가 개선되었다는 패치 내역을 발견했다.

https://www.apachelounge.com/changelog-2.4.html

mod_proxy_fcgi: Honor "SetEnv proxy-sendcl" to forward a chunked
Transfer-Encoding from the client, spooling the request body when needed
to provide a Content-Length to the backend. PR 57087

 

  • proxy-sendcl 옵션: chunked 요청을 받아 프록시가 Content-Length를 계산하여 백엔드로 전달
  • 기본값은 proxy-sendchunks
  • 우리 서버(Apache/2.4.62)는 버전은 충분하지만, proxy-sendcl 옵션은 httpd.conf에 설정되어 있지 않았음

📌📌 드디어.. 공식 문서에서 원인을 찾았다! 이제 ENV 추가 후 테스트 해볼 일만 남았다! 💪

 

8️⃣ (추가 궁금증) Java WebClient에서 chunked 전송

우리 서버에서 env 추가를 통해 궁극적인 문제를 해결할 수 있게 되었다 :)

다만 아직 궁금증은 남아있었다. '왜 요청지에서는 고정적으로 chunked 처리해서 보낼까?'

 

스프링 쪽 경험이 많지 않아 정확히는 모르겠지만,

Java WebClient에서 요청할 때 chunked 전송이 기본적으로 적용되는 경우가 있는지 궁금했다.

 

찾아보니, multipart/form-data 형식으로 전송하는 경우에는

내부적으로 Flux<DataBuffer> 형태로 body를 직렬화하게 되며,

이 때문에 Content-Length를 미리 계산할 수 없어서 자동으로 chunked 전송이 적용된다고 한다.

https://velog.io/@shdbtjd8/Spring-WebClient-%EC%A3%BC%EC%9D%98%EC%A0%90

 

그래서 특정 요청지의 API 콜백 요청에서만 간헐적으로 에러가 발생했구나.. 라는 최종적인 퍼즐까지 맞춰졌다 ㅎㅎ

 

🏁 마무리

  • 원인: Apache + mod_proxy_fcgi + PHP-FPM 조합에서 chunked 전송 처리 실패
  • 재현 케이스: Java WebClient multipart/form-data → 자동 chunked
  • 해결: Apache 환경에 proxy-sendcl 옵션 적용

 

2-3일 동안 정말 열심히 디버깅한 결과.. 2-3년 동안 묵은 이슈를 해결할 수 있게 되어 넘 뿌듯했다 ㅎㅎ

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.