[body-parser] 게이트웨이 서버와 body-parser 그리고 raw-body
최근 회사에서 유저 인증과 게이트웨이 역할을 하는 인증 게이트웨이 서버를 사용했다.
간단하게 설명하면 인증 게이트웨이 서버는 로그인을 하는 역할을 하며 동시에 로그인이 된 유저는 특정 endpoint로 요청을 하고 해당 요청을 원하는 endpoint로 중계해 주는 역할을 하는 서버이다.
API연동을 하는 도중 프론트팀에서 GET요청에 대해서는 문제없이 동작하는데 POST, PUT이 동작을 안 한다는 것이었다.
특정 에러 코드도 없이 그냥 pending 걸리다가 timeout이 났다. 서버쪽 로그를 확인해 봐도 이상이 없었다.
진짜 도저히 모르겠어서 팀장님께 말씀드려보니 아 그거 body-parser에서 문제 있는 거 아니에요? 하셨다.
body-parser? 왜지..? 하다가 오류를 수정하기 위해 찾아본 내용과 body-parser모듈에서 확인한 내용을 기록하려 한다.
Stream에 대하여
일단 위 내용을 어느정도 이해하려면 stream에 대한 지식이 필요한데 나도 아직까지는 stream에 대해 정확하게 알지 못해 누군가에게 설명할 수 있는 정도의 실력은 안된다 생각하여 Chat GPT에게 물어본 내용을 공유한다. 다음번에는 stream에 대해 이해하기 위해 stream을 만들어보면 어떨까 함.
스트림은 데이터를 일정한 속도로 청크(chunk) 단위로 전달하는 방식으로 동작합니다.
데이터가 스트림을 통해 흐르면서 버퍼 또는 네트워크 등에서 데이터를 읽고 처리합니다.
이러한 동작 방식은 데이터의 효율적인 처리와 메모리 사용을 가능하게 합니다.
스트림은 일방향적인 흐름을 가지므로 데이터가 스트림을 통해 전달된 후에는
스트림이 끝에 도달하거나 닫히면 해당 데이터는 버려집니다.
스트림은 순차적으로 데이터를 전달하는 개념이기 때문에 이전에 읽은 데이터는 다음에 다시 읽을 수 없습니다.
또한, 스트림은 리소스를 효율적으로 사용하기 위해 데이터를 일정한 속도로 처리하므로
버퍼에 저장된 데이터를 모두 소비한 후에는 추가 데이터를 읽어들이지 않습니다.
스트림은 일반적으로 데이터를 스트리밍하면서 실시간으로 처리하거나 순차적으로 데이터를 소비하는 용도로 사용됩니다.
따라서 스트림은 데이터의 효율적인 처리와 메모리 사용을 위해
한번 읽힌 데이터는 다시 읽을 수 없는 구조로 설계되어 있습니다.
만약 스트림에서 데이터를 다시 읽어야 한다면, 데이터를 별도로 저장하거나 버퍼에 저장하여 사용해야 합니다.
여기서 가장 중요한 정보는 스트림은 일방향적인 흐름을 가지므로 데이터가 스트림을 통해 전달된 후에는 스트림이 끝에 도달하거나 닫히면 해당 데이터는 버려집니다. 이다
한번 읽힌 스트림은 다시 읽힐 수 없다. 기억하기
body-parser
body-parser은 npm 모듈 중에 가장 유명한 모듈이라고 생각한다. 뭐 express로 서버를 열어주고 request를 받아주는 거의 모든 서버가 body-parser를 쓰지 않을까? 당장 npm만 확인해도 엄청난 다운로드 수를 확인할 수 있다.
동작 원리
1. http request는 원래 객체가 아니라 스트림이다
2. request 스트림을 전달받은 body-parser에서 request 스트림을 읽고 js에서 사용할 수 있는 request 객체로 만들어 준다
return function jsonParser (req, res, next) {
if (req._body) {
debug('body already parsed')
next()
return
}
...
// read
read(req, res, next, parse, debug, {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
})
}
3. 위 코드에서 read 함수는 스트림을 raw-body라는 모듈의 getBody 함수에 콜백 함수와 함께 전달해서 request를 객체화해주는 함수이다.
function read (req, res, next, parse, debug, options) {
var length
var opts = options
var stream
// flag as parsed
// read options
// set raw-body options
// assert charset is supported
// read body
debug('read body')
getBody(stream, opts, function (error, body) {
// verify
...
// parse
var str = body
try {
debug('parse body')
str = typeof body !== 'string' && encoding !== null
? iconv.decode(body, encoding)
: body
req.body = parse(str)
} catch (err) {
next(createError(400, err, {
body: str,
type: err.type || 'entity.parse.failed'
}))
return
}
next()
})
}
4. raw-body라는 모듈은 node의 stream기능을 이용해서 스트림이 전달되면 특정 버퍼에 저장을 했다가 스트림이 end 되었다는 이벤트에 특정 버퍼에 저장했던 내용을 전달하는 기능을 한다.
문제가 발생한 이유
프론트의 POST, PUT등 body를 가진 모든 요청이 인증 게이트웨이 서버에 도달하게 되고 인증 게이트웨이 서버에서 body-parser모듈을 통해 request 스트림이 객체화가 되어 버렸다.
이 상태의 request는 API서버로 전달되고 API서버에서도 body-parser를 사용을 하면서 문제가 되었다.
API서버에서 body-parser가 전달받은 request는 스트림이라는 기댓값을 가지고 있다.
하지만 전달받은 request는 스트림이 아니라 객체다. 이러한 request는 이벤트리스너를 추가하더라도 close 이벤트만 일어난다.
이 close 이벤트만 발생하는 게 문제를 야기시킨다.
이것이 왜 문제가 되는지 알기 위해서는 위에서 보았던 raw-body의 동작에 대해서 조금 더 살펴보아야 한다.
function readStream (stream, encoding, length, limit, callback) {
...생략
var buffer = decoder
? ''
: []
// 이벤트 리스너 등록
stream.on('aborted', onAborted)
stream.on('close', cleanup)
stream.on('data', onData)
stream.on('end', onEnd)
stream.on('error', onEnd)
// mark sync section complete
sync = false
// 이 done 함수가 호출되어야 콜백 함수 실행이 가능함
function done () {
var args = new Array(arguments.length)
// copy arguments
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i]
}
// mark complete
complete = true
if (sync) {
process.nextTick(invokeCallback)
} else {
invokeCallback()
}
function invokeCallback () {
cleanup()
if (args[0]) {
// halt the stream on error
halt(stream)
}
callback.apply(null, args)
}
}
function onAborted () {
if (complete) return
done(createError(400, 'request aborted', {
code: 'ECONNABORTED',
expected: length,
length: length,
received: received,
type: 'request.aborted'
}))
}
function onData (chunk) {
버퍼에 데이터 저장
}
function onEnd (err) {
if (complete) return
if (err) return done(err)
if (length !== null && received !== length) {
done(createError(400, 'request size did not match content length', {
expected: length,
length: length,
received: received,
type: 'request.size.invalid'
}))
} else {
var string = decoder
? buffer + (decoder.end() || '')
: Buffer.concat(buffer)
done(null, string)
}
}
function cleanup () {
buffer = null
stream.removeListener('aborted', onAborted)
stream.removeListener('data', onData)
stream.removeListener('end', onEnd)
stream.removeListener('error', onEnd)
stream.removeListener('close', cleanup)
}
}
위 코드에서 done 함수의 역할은 전달받은 콜백을 실행시켜 주는 역할을 한다.
이 done이 실행이 되어야 stream을 갖고 body로 만들어주던 뭘 하던 할 수 있다.
그런데 잘 살펴보면 done 함수를 호출하는 함수는 이벤트 리스너에 'end', 'aborted' 이벤트로 등록된 onEnd, onAborted 함수뿐이다.
이 모든 것을 종합해 보면
- 이미 객체화된 request는 스트림 이벤트 리스너에서 'close' 이벤트만 발생한다
- raw-body에서는 'end', 'aborted' 이벤트에 대해서만 다음 함수로 이어질 수 있다
- raw-body에서 'close' 이벤트에 등록된 cleanup 함수만 실행되었다
- body-parser의 동작이 중간에 멈췄다
- 이로 인해 request에 별 다른 에러도 확인할 수 없었고, pending상태에서 결국에는 timeout만 발생하였다
마지막 정리
row-body 모듈을 까보다 보니 cleanup만 일어나는 경우에도 뭔가 에러처리를 해주면 좋을 것 같다는 생각이 들었다.
사실 코드를 이해하는 정도로 그쳤고 정확한 동작은 조금 더 복잡하겠지만 cleanup동작만 일어나서 어떠한 에러 메시지도 전달받을 수 없는 경우를 막기 위해 에러를 발생시켜 주는 코드를 추가해서 PR을 해보면 어떨까 하는 생각이 든다.
raw-body 깃헙에서 이슈들을 보다 보면 분명 누군가가 cleanup동작에 에러를 추가하는 게 어떻겠냐는 의견이 있을 것 같은데 왜 추가하지 않았는지 raw-body 측의 의견을 보면 어떠한 관점을 갖고 모듈을 만들었는지 알 수 있지 않을까 싶다.
뇌피셜은 body-parser 또한 raw-body를 스트림 유틸로 가져다 쓴 거고 raw-body 측에서는 body-parser를 고려하지 않고 모듈을 만들었기 때문 같기도 하다. raw-body는 단순 스트림 유틸이니까 body-parser에서 발생시킬 수 있는 에러를 고려하지 않았을 듯하다.
그러면 PR은 body-parser에 남겨야 하는 것인가..?
혹시나 싶어서 raw-body PR을 찾아보니까 내가 원하는 내용의 PR이 있었고 stream.readable을 체크하는 PR이 22년 2월에 Merge 된 이력을 확인할 수 있었다.
[참고링크]
body-parser: https://github.com/expressjs/body-parser
raw-body: https://github.com/stream-utils/raw-body
PR링크: https://github.com/stream-utils/raw-body/pull/58
check stream readable by ziyofun · Pull Request #58 · stream-utils/raw-body
Add check for stream in situation #57 ,if the stream.readable is false, there will be an error instead of endless waiting.
github.com
GitHub - stream-utils/raw-body: Get and validate the raw body of a readable stream
Get and validate the raw body of a readable stream - GitHub - stream-utils/raw-body: Get and validate the raw body of a readable stream
github.com
GitHub - expressjs/body-parser: Node.js body parsing middleware
Node.js body parsing middleware. Contribute to expressjs/body-parser development by creating an account on GitHub.
github.com