Express에서 Nest로 마이그레이션 중, 예상치 못한 오류가 생겼다.
프록시 부분을 제일 마지막에 진행하게 되어서 마이그레이션 테스트 중, 웹에서 400 Bad Request가 뜨는 것. ping을 날려보면 서버는 잘 켜져 있다. data form이 잘못 전달된건가 하고 프론트에서 Axios 세팅을 수정해보던 중… “Express에선 잘 되더니, Nest로 마이그레이션하니까 안 되는 거야? 프론트는 그대로인데 왜?”
정확한 에러 상황 파악하기
NestJs 기반의 서버 앞단에 Gateway(proxy server)가 있고 프론트에서 /login 경로로 POST 요청을 Gateway로 보낸 후, Gateway가 http-proxy-middleware를 통해 이 요청을 NestJS서버의 /login endpoint로 프록시한다. 이 과정에서 NestJS 서버 측 로그에 아래와 같은 에러가 발생했다.
BadRequestError: request aborted
at IncomingMessage.onAborted (/node_modules/raw-body/index.js:245:10)
로그 상으로 이 에러는 body-parser 내부 모듈인 raw-body에서 발생한 것이다. 요청이 중단됨(request aborted), 이로 인해 NestJS의 ExceptionHandler가 해당 오류를 400 Bad Request로 처리했다.
Body-Parser ?
요청(Request)의 본문을 파싱해서 req.body에 넣어주는 Node.js의 미들웨어.
body 데이터를 읽고 json이나 text 등 원하는 형식으로 변환한다. 이 과정에서 요청 본문을 소비하게 된다.
*본문은 단 한 번만 읽을 수 있다.
BadRequestError: request aborted - 주요 발생 케이스
1️⃣ 요청 중단
서버는 Content-Length 크기 또는 Transfer-Encoding 헤더를 보고 본문 길이를 예측하고 데이터를 기다리는 중, 클라이언트가 연결을 끊거나 중간에서 본문이 조기 종료되어버리는 경우
2️⃣ 프록시 서버에서 요청 본문 소비
프록시 서버에서 express.json(), body-parser.json()과 같은 미들웨어가 프록시 앞단에서 body를 파싱해 버린 경우
3️⃣ Content-length 헤더와 실제 body 크기 불일치
클라이언트 또는 프록시가 Content-Length를 잘못 설정한 경우
4️⃣ HTTP Parser 오류 또는 헤더 누락/오류
Content-Type이 잘못 지정된 경우 body-parser가 파싱을 시도하지 못해 내부적으로 실패할 수 있음
프론트엔드 코드는 동일하고, NestJS 단독 서버는 정상 응답이었으며, Timeout이 발생하지 않았고 오직 Gateway 프록시를 경유했을 때만 문제가 발생하는 것으로 보아 2번 케이스로 생각되었다.
Gateway 서버에서 Body-parser를 적용해 프론트엔드 요청의 본문을 파싱하면, 요청 스트림이 한 번 소모되어 버린다. 그 후 http-proxy-middleware가 해당 요청을 NestJS로 전달하려 해도, 이미 읽힌 스트림에는 보낼 데이터가 없다.
NestJS 쪽에서는 Content-Length 헤더에 따라 본문 데이터를 기다리지만 실제 데이터가 도착하지 않으므로, 요청이 종료되어 버린다. 그 결과 raw-body 모듈이 "BadRequestError: request aborted" 예외를 던지게 되는 것.
해결 방법
📌 전역 body-parser 비활성화 또는 조건부 적용
await NestFactory.create(GatewayModule, {
cors: true,
bodyParser: false, // Nest가 body 파싱하지 않도록
});
NestJS를 게이트웨이로 사용하는 경우 NestFactory.create()에서 bodyParser: false 옵션을 주어 전역 바디파서를 꺼준다.
이렇게 하면 /login과 같이 프록시로 전달할 경로의 요청은 게이트웨이에서 본문을 읽지 않고 그대로 NestJS 서버로 전달하게 되어, 원본 스트림이 유지된다. NestJS API 게이트웨이 구현 시 특정 경로에만 프록시를 적용하고 그 외 경로에는 바디파서를 조건부로 적용하는 패턴도 있다
📌 http-proxy-middleware의 fixRequestBody 활용
import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
createProxyMiddleware({
target: 'http://base:7488',
changeOrigin: true,
pathRewrite: { '^/login': 'login' },
on: {
proxyReq: fixRequestBody,
},
});
fixRequestBody ?
프록시 서버에서 이미 파싱된 요청 본문을 다시 원래의 형태로 복원하여 대상 서버로 전달하는 데에 사용되는 유틸리티 함수.
fixRequestBody 내부에서는 req.body가 있을 때만 동작하며, JSON의 경우 JSON.stringify를 통해 문자열로 변환한 뒤 Content-Length 헤더를 Buffer.byteLength로 재계산하고 본문을 proxyReq 스트림에 써준다.
req.body가 없거나 아직 스트림에 읽히지 않은 데이터가 남아있는 경우에는 아무 것도 하지 않고, 원래 스트림을 그대로 사용하도록 넘어간다.
내가 선택한 방법 + fixRequestBody의 업데이트 사항
당시 문제를 가장 빠르게 해결할 수 있는 방법으로 fixRequestBody를 선택했다. application/json 타입에는 body-parser를 간단히 적용할 수 있지만 multipart/form-data를 처리하기 위해선 엄청 귀찮아졌다. multer기반의 multipart 전용 파서를 사용해야했다. fixRequestBody는 조건부 분기 처리로 multipart 요청은 건드리지 않도록 구현하기만 하면 되었다.
createProxyMiddleware({
target: 'http://base:7488/',
changeOrigin: true,
pathRewrite: { '^/login': 'login' },
...(req.headers['content-type']?.startsWith('multipart/form-data')
? {}
: {
on: {
proxyReq: fixRequestBody, // multipart가 아닌 경우에만 적용
},
}),
});
근데 포스팅을 위해 다시 확인해보니 코드 적용한지 10일 후, 불과 현재로부터 2달 전에 해당 부분이 업데이트되었다. LUCKY 🍀
심지어 다른 타입도.
이제는 fixRequestBody가 multipart까지도 지원하게 되었기 때문에, 조건부 분기 없이도 대부분의 요청 타입에서 안정적으로 동작한다.
// Use if-elseif to prevent multiple writeBody/setHeader calls:
// Error: "Cannot set headers after they are sent to the client"
if (contentType.includes('application/json') || contentType.includes('+json')) {
writeBody(JSON.stringify(requestBody));
} else if (contentType.includes('application/x-www-form-urlencoded')) {
writeBody(querystring.stringify(requestBody));
} else if (contentType.includes('multipart/form-data')) {
writeBody(handlerFormDataBodyData(contentType, requestBody));
} else if (contentType.includes('text/plain')) {
writeBody(requestBody);
}
}
총 정리
프록시만 할 땐 body-parser : false가 깔끔하고,
본문을 읽거나 요청 타입에 multipart 등 다양한 타입이 섞여있을 경우 fixRequestBody로 안전하게 복원해서 사용하기!
다시 들여다본 덕분에 업데이트 사항을 확인할 수 있었다. 더더욱 fixRequestBody를 쓰지 않으면 안되는 이유가 생긴 느낌
그치만 역시나 간단하게 가져갈 수 있다면 body-parser false 적용만큼 쉬운게 있을까
실무에서 배운 것을 다시 들여다 보는 것 만으로 이렇게 값진 공부를 할 수 있음을 다시 알게 되었다.
요즘은 다시 Front 소양을 쌓기 위해 노력해야겠다고 절실히 느끼는 중이라,,,
언젠가 다시 Nest 공부로 찾아올 수 있기를!
Request stalls when proxying request unless I use http-proxy-middleware
fix-request-body.ts (http-proxy-middleware GitHub)
Nest.js의 Request lifecycle feat. body-parser가 proxy를 망친다 (Velog)
http-proxy-middleware v2.0.6 - Intercept and Manipulate Requests
body-parser middleware in Node.js (GeeksForGeeks)
StackOverflow - else 블록 이후 요청이 멈추는 문제
How to reproduce BadRequestError: request aborted
http POST body not attached in the proxied request
Node.js http.IncomingMessage aborted method (GeeksForGeeks)
'Problem Drilling' 카테고리의 다른 글
node.js express/multer 사용시 UTF-8 filename 미지원 이슈 해결 과정 (8) | 2024.10.13 |
---|---|
브라우저에서 Excel 파일 다운로드하기: Blob과 CORS 해결과정 (1) | 2024.04.22 |
Axios interceptors로 API Data 목적에 맞게 분류하기 (1) | 2024.03.03 |
Github Action Test 성공하고 Workflow 복붙했다가 큰 코 다친 이야기 (3) | 2023.11.12 |
Github Actions 사용, Workflow 테스트 자동화 적용하기, pnpm (0) | 2023.11.11 |