かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Node.js Express のレスポンスで Unhandled Promise Rejection エラーが発生

Express で作った API を Lambda を実行していたらログに次のようなエラーが出力されていました。
Unhandled Promise Rejection Error: [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
Lambda からは問題なく JSON が返ってきていたのでで問題はないのですが、エラーをこのままにしておくのは気持ち悪い…

Unhandled Promise Rejection

Unhandled Promise Rejection はざっくりいえば Promise が reject された際に cactch する処理が無い場合に派生するエラーです。 詳しい仕様に関しては こちらのブログ が大変参考になりました。

今回の場合はエラーの文面にクライアントに送信した後に Header をセットしようとしてあると書かれています。
これは下記のようにエラーハンドリングや場合分けでレスポンスを返す際に returnを書き忘れた時によく出くわすエラーです。

if (!token) {
  res.send({message: 'Incorrect token.'});
}
res.send({message: 'success'});

token が無い時に早期 retuen のつもりでレスポンスを返しているが、res.send は処理を止めないので、そのまま次の res.send が実行されレスポンスの二重送信になってしまう。
👇 return を使って処理を止めるようにすれば OK

if (!token) {
  return res.send({message: 'Incorrect token.'});
}
return res.send({message: 'success'});

Response の二重送信が原因だった

今回のケースは Router で実行する関数を async 関数にしていたので、おそらく二重にレスポンスを返してしまっている箇所があり二回目のレスポンスがエラーとなり、これが Promise.reject 扱いとなり Unhandled Promise Rejection になっているものと推察しました。

エラーが発生していたコード

router.get(
  '/api/foo',
  async(req: Request, res: Response<IAccount>, next: NextFunction) => {
    //  色々処理
    return res.json({ ...responseData }).sendStatus(200);
});

// Promise.reject をキャッチしてエラーのレスポンスを返す
app.use(errorHandler);

router 内で発生した例外は errorHandler でキャッチする構成。(参考 Express な API でエラーハンドリングするよ - かもメモ)
正常にレスポンスが返ってきている場合もエラーが発生していた。

res.json(), res.sendStatus() は両方ともレスポンスを返すメソッドだった

res.json([body])
Sends a JSON response. This method sends a response (with the correct content-type) that is the parameter converted to a JSON string using JSON.stringify().

res.sendStatus(statusCode)
Sets the response HTTP status code to statusCode and sends the registered status message as the text response body. If an unknown status code is specified, the response body will just be the code number.
cf. Express 5.x - API Reference

ドキュメントを読むと res.json(), res.sendStatus() 共にレスポンスを返すメソッドだと書かれていました。 つまり res.json().sendStatus() としてしまっている箇所が Json を返した後に status 200 だけのレスポンスを返す処理になってしまっていたのが原因でした…

Status Code の設定は status() を使おう

res.status(code)
Sets the HTTP status for the response. It is a chainable alias of Node’s response.statusCode.

router 内のレスポンス部分を下記のように修正すれば OKでした

router.get(
  '/api/foo',
  async(req: Request, res: Response<IAccount>, next: NextFunction) => {
    //  色々処理
-   return res.json({ ...responseData }).sendStatus(200);
+   return res.status(200).json({ ...responseData });
});
所感

今回は本当に単純ミスでした…

教訓。ドキュメントをちゃんと読もう!

おわり。


[参考]