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 }); });
所感
今回は本当に単純ミスでした…
教訓。ドキュメントをちゃんと読もう!
おわり。
[参考]
- Unhandled Rejection の考え方 - from scratch
- node.js - ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client - Stack Overflow
- Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client - How to fix? - Poopcode
- res.sendStatus(statusCode) | Express 5.x - API Reference
- res. json([body]) | Express 5.x - API Reference