かもメモ

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

JavaScript Express async function の UnhandledPromiseRejectionWarning にハマる。

Express で簡単な JWT の API を作って実験していた際に middleware を async function に変更したら UnhandledPromiseRejectionWarning が出るようになってハマってしまったのでメモ

UnhandledPromiseRejectionWarning になったコード

// router
const router = require('express').Router();
const verify = require('./verify');

router.get('/', verify, (req, res) => {
  res.send(req.data);
});

module.exports = router;

middleware

// verify.js
const jwt = require('jsonwebtoken');
const { checkTokenValidity } = require('./token');

module.exports = async function(req, res, next) {
  const token = req.header(TOKEN_HEADER);
  if (!token) {
    return res.status(401).send('Access Denied');
  }

  try {
    // 有効期限切れなどで verify ができないと catch に処理が流れる
    const verified = jwt.verify(token, TOKEN_SECRET);
    
    // token を redis に問い合わせて revoke されてないかチェックする
    const isValidity = await checkTokenValidity(token);
    if (!isValidity) {
      throw new Error('Token is revoked');
    }
    
    req.data = verified;
    next();
  } catch(err) {
    res.status(400).send('Invalid Token');
  }
}

token が渡されない場合や、token が有効な時は問題がないのですが、token が有効でなく Invalid Token になる場合に UnhandledPromiseRejectionWarning が発生します。

note. UnhandledPromiseRejectionWarning

UnhandledPromiseRejectionWarning は非同期処理の Promise でエラーが catch できなかった時に発生するエラー

非同期関数の await checkTokenValidity(token) を追加した後から、この warning が発生するようになったので、この関数が悪いのかと思い別途 try 〜 catch で囲ってみたり、コメントアウトしてみたり、Promise.reject() を書いてみたりしても変化がなく、どうやら middleware を async function にした事で warning が発生するようになっていました。

HEADER が 2重に返されていたのが原因だった

長い warning をよく読んでみると ERR_HTTP_HEADERS_SENT という記述があり、どうやら res.send() された後に更に res.send() がされているのがこの warning 発生の直接的な原因となっているようでした。

async function 内では明示的に return する必要がある

middleware が async function でない場合は next() が呼ばれず res.send() してしまえば、その場で処理が終了になっていたものが、async function にしたことで返される値を待つようになってしまったので、res.send() で処理が終了にならず、関数の最後に返される undefinedPromise.resolve() として受け取っていたということでした。

// verify.js
module.exports = async function(req, res, next) {
  // ...
  try {
    // …
  } catch(err) {
    res.status(400).send('Invalid Token');
  }
  // ここで即時 undefined が return される
}

// router
router.get('/', verify, (req, res) => {
  // verify が async function なので、return を待つのでエラーの場合
  // 関数の最後で返される undefined を Promise.resolve として次の res.send() が呼ばれる
  res.send(req.data);
});

async function な middleware で res.send する時は明示的に return をする

明示的に return res.send() をすればOK

// verify.js
module.exports = async function(req, res, next) {
  // ...
  try {
    // …
  } catch(err) {
    // エラーの場合はこれが返されて関数は終了
    return res.status(400).send('Invalid Token');
  }
  // ここに到達することはない
}

// router
router.get('/', verify, (req, res) => {
  // エラーの場合先に res.send() が返されて実行されているので、res.send(req.data) は実行されない
  res.send(req.data);
});

 
単純に async / await の挙動の考慮漏れが原因でした…
小一時間ほどハマってしまった。


[参考]

アイドルタイムプリパラ☆ミュージックコレクション

アイドルタイムプリパラ☆ミュージックコレクション

  • アーティスト:V.A.
  • 発売日: 2018/06/27
  • メディア: CD

プリパラの履修始めました。