react-native-multiple-image-picker라이브러리를 사용해 사용자 앨범에서 이미지 선택 후 formdata 처리하여 fetch/axios HTTP 통신으로 서버에 전송 시 Network request failed 문제가 발생하였다.

더욱 골치 아팠던 것은 해당 에러가 간헐적으로 발생한다는 것이었다. 성공하면 항상 성공하고 실패하면 항상 실패하는 것이 지금까지 당연한 진리였는데 이렇게 간헐적으로 문제가 발생하니 원인을 파악하기 더 어려웠다.

Network request failed의 대표적인 원인으로 3가지가 있는데, 

1. URL 잘못입력한 경우 

2. 서버 보안 문제

3. 클리어텍스트 트래픽 문제 

위 3가지 모두 나에게 해당하는 원인은 아니었다.

 

axios, fetch 이외에 XMLHttpRequest, RNFtechblob등 다양한 HTTP 통신 라이브러리를 시도한 결과 RNFetchBlob에서 Broken pipe/ Steam Closed라는 새로운 오류문이 출력되는 것을 확인하였다. 드디어 너무나 불친절한 Network request failed 오류문에서 벗어나게 되었다.

 

[Broken pipe/ Steam Closed]

■1.Caused by server side

클라이언트와 서버가 통신 중에 예기치 못하게 소켓이 끊어지면 발생하는 문제였다. 먼저 서버 쪽 문제인지 클라이언트 문제인지 파악해야했고 처음에는 서버쪽을 의심했다. 서버 쪽 콘솔을 확인한 결과 클라이언트에서 보낸 요청 자체는 서버쪽에서 제대로 받고 있었고 multer 미들웨어 이후 controller에 진입하지 않는 것으로 보아 multer 미들웨어를 의심하게 됐다.  

404등의 status가 아닌 --ms--로 출력되어 당황스러웠지만 검색결과 아래와 같은 경우 --ms--가 출력된다고 한다.

“essentially means that you never sent a response before Node.js killed the TCP connection for idling too long”

이 내용을 확인한 후 더욱더 서버 쪽 정확히는 multer의 문제라고 생각했다. 내가 의심한 원인은 크게 아래 두 가지 였다.

1.multer가 클라이언트에게 formdata를 받는 것이 오래 걸려 task를 완료하기 전에 요청에 대한 timeout이 발생하여 서버 측에서 connection을 닫아 버림.

2.multer가 formdata를 받는 것엔 성공했으나 S3에 업로드가 오래 걸려 클라이언트에게 res전달이 지연되며 문제 발생. 따라서, 요청에 대한 timeout을 늘려줘야 될 것으로 예상.

 

하지만,  Node.js 공식문서를 참조한 결과

v13.0.0부터 "The default timeout changed from 120s to 0 (no timeout)." 즉, default timeout이 2분이었다. 나의 경우 문제 에러가 출력되는데 까지 수초가량밖에 걸리지 않았고 에초에 이미지 데이터 자체도 kb 단위 크기로 절대 오랜 시간이 필요한 만큼 큰 데이터가 아니었기에 다른 문제라고 판단했다. 실제로, timeout을 5분으로 늘려주어도 동일한 문제가 계속해서 발생했다.

 

문제를 해결하는데 중요한 것은 유연한 사고!! 에초에 서버 쪽 문제라고 단정 지은 것부터가 잘못된 판단이라고 생각하여 내가 처한 문제를 처음부터 다시 고찰한 결과 이번엔 클라이언트쪽을 의심하게 되었다.

 

■2.Caused by client side

Three way handshake

three way handshake란 간단하게 클라이언트와 서버가 통신할 때 connetion을 establish하는 과정을 뜻한다. 양측에서 SYN과 ACK을 주고 받으며 connection을 설립하는 이 과정은 보기엔 간단해 보이지만 생각보다 많은 cost가 발생한다. 이때문에, Node.js에서는 keep-alive time을 설정하여 multiple-request에 대한 connection의 재사용을 보장할 수 있도록 한다.

HTTP1.1

Three way hasnshake cost를 줄이고 TCP의 재사용성을 보장하기 위한 HTTP 버전으로 내가 사용중인 HTTP 버전을 확인하기 위해 서버 측 logger를 수정해주었다.

var logger = require("morgan");
app.use(logger("common"));

 

다시 문제로 돌아가보면, 나의 경우

1.이미지를 서버에 전송하고

2.서버에서 AWS S3에 이미지를 올린 후

3.업로드한 이미지의 url경로를 프론트에 res로 반환.

4.프론트에서는 url경로와 nickname등과 함께 변경한 프로필 정보를 body에 담아 다시 서버에 요청을 보내는 로직인데,

연속적으로 빠르게 프로필을 변경하려 하는 경우 처음 요청 때 생성된 Socket의 자원을 httpThread.run()에서 사용하려고 하면서, 첫 번째 요청이 완료되기 전 두 번째 재요청이 발생하여 이전 Socket이 끊어지고 예치기 못한 stream 종료 즉, 해당 오류를 발생시키는 것이었다.

 

Try

문제를 파악한 후 해결책을 고민해 보았다. 

1.HTTP/1.0 버전 사용

동일한 Host와 포트를 가진 HTTP 요청일지라도 connection을 재사용하지 않고 매번 새로운 connection을 생성해주면 문제 자체는 해결할 수 있지만. 매번 handshake cost가 발생하기 때문에 이 방법은 지양하였다.

2.HTTP/2.0 버전 사용

순서대로 HTTP/1.0/1.1/2.0의 통신 과정을 간력하게 도식화 한 그림이다. 2.0버전을 사용하면 multiple-request가 독립, 병렬적으로 수행될 수 있기 때문에 문제를 해결 할 수 있을 것이라 생각했다. 하지만 이 역시 해결되지 않았는데, 생각보다 허무하게 문제를 해결했다. 요청에 대한 소켓이 끊어져 stream이 종료되는 것 자체는 잘 파악했지만 원인은 요청에 대한 응답을 처리하는 어떠한 로직도 없이 바로 페이지가 이동되며 요청에 대한 연결이 끊어지는 것이었다... 이후 응답 처리 로직을 작성하여 문제를 해결할 수 있었다.