kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

expressのエラーハンドリングについて少し理解する①

f:id:kun432:20220213014606p:plain

引き続き、express。今日はエラーハンドリングについていろいろやってみる。

目次

同期処理のエラーのキャッチ

まずは公式ドキュメントをお手本に写経的にやってみる。

コードはこれをベースに。morgan-bodyお手軽で気に入ったのでデフォで入れてます。

const express = require('express')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/', (req, res) => {
  res.json({msg: "hello, world"})
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

こんな感じ。

$ curl -X GET http://localhost:3000/?foo=1\&bar=2
{"msg":"hello, world"}

$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }'
{"msg":"hello, world"}

$ curl -X GET http://localhost:3000/aaa
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /aaa</pre>
</body>
</html>

ちなみに上で最後に実行したルートに当たらない404みたいなエラーハンドリングは今回やってみるケースとは違う。ここは後ほど。

どのようにして 404 応答に対応するのですか?

Express では、404 応答はエラーの結果ではありません。そのため、エラー・ハンドラー・ミドルウェアはそれらをキャプチャーしません。このように動作するのは、404 応答は単に追加の処理が存在しないことを示しているためです。つまり、Express は、すべてのミドルウェア関数とルートを実行して、そのいずれも応答しなかったことを検出したということです。404 応答に対応するには、スタックの最下部 (他のすべての関数の下) にミドルウェア関数を追加するだけですみます。

app.use(function(req, res, next) {
    res.status(404).send('Sorry cant find that!');
});

Express に関する FAQ

まず、同期処理。

const express = require('express')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

// ココ!
app.all('/', (req, res) => {
  throw new Error('BROKEN') // Express will catch this on its own.
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

実際に実行してみるとこうなる。

$ curl -X GET http://localhost:3000/?foo=1\&bar=2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: BROKEN<br> &nbsp; &nbsp;at /Users/kun432/repository/mac-redis/index.js:11:9<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at Route.dispatch (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:112:3)</pre>
</body>
</html>

$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: BROKEN<br> &nbsp; &nbsp;at /Users/kun432/repository/mac-redis/index.js:11:9<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> &nbsp; &nbsp;at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)</pre>
</body>
</html>

普通にエラーをスローするだけでよしなにやってくれてるのがわかる。ちなみにルート外だとこうなるので、これは別物だということがよくわかる。

$ curl -X GET http://localhost:3000/aaa
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /aaa</pre>
</body>
</html>

非同期処理のエラーのキャッチ

ルートハンドラ内で非同期処理のエラーを呼び出してみる。この場合はエラーをキャッチしてnext()に渡す必要がある。

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/', (req, res, next) => {
  fs.readFile('./file-does-not-exist', 'utf-8', function (err, data) {
    if (err) {
      next(err) // Pass errors to Express.
    } else {
      res.send(data)
    }
  })
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

結果

$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: ENOENT: no such file or directory, open &#39;./file-does-not-exist&#39;</pre>
</body>
</html>

ファイルがあればもちろん問題ない

$ echo "hoge" > ./file-does-not-exist
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }'
hoge

ミドルウェアを使って書き直すとこんな感じかな。そういうものなのかわからないけど、res.localsを使ってミドルウェアでの結果をルートハンドラに受け渡していて、ミドルウェア内でのエラーもちゃんと補足できた。

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

const readFile = (req, res, next) => {
  fs.readFile('./file-does-not-exist', 'utf-8', function (err, data) {
    res.locals.data = data
    next(err);
  });
};

app.use(readFile);
app.all('/', (req, res) => {
  res.send(res.locals.data);
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

こんな書き方もできる。

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/',
  (req, res, next) => {
    fs.readFile('./file-does-not-exist', 'utf-8', function (err, data) {
      res.locals.data = data;
      next(err);
    });
  },
  (req, res) => {
    res.send(res.locals.data);
  }
)

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

コールバックでデータの処理が必要なく、単にエラーを発生する場合はエラーの受け渡しとか気にせずに、簡略的にかける。この場合はエラーがあってもなくてもnextが呼び出され、エラーがない場合は次のハンドラ、エラーの場合はexpressがキャッチしてくれる。

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/',
  (req, res, next) => {
    fs.writeFile('/inaccessible-path', 'data', next)
  },
  (req, res) => {
    res.send('OK');
  }
)

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

実行

$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: EROFS: read-only file system, open &#39;/inaccessible-path&#39;</pre>
</body>
</html>

非同期の例、もう一つ。try...catchを使った場合もエラーをキャッチしてnext()に渡してあげる。

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('BROKEN');
    } catch (err) {
      next(err);
    }
  }, 5000)
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
});
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }'
(...5秒後...)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: BROKEN<br> &nbsp; &nbsp;at Timeout._onTimeout (/Users/kun432/repository/mac-redis/hello2.js:14:13)<br> &nbsp; &nbsp;at listOnTimeout (node:internal/timers:559:17)<br> &nbsp; &nbsp;at processTimers (node:internal/timers:502:7)</pre>
</body>
</html>

try...catchを省くとどうなるか?

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/', (req, res, next) => {
  setTimeout(() => {
      throw new Error('BROKEN');
      next(err);
  }, 5000)
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
});

結果

$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }'
curl: (52) Empty reply from server

expressがエラーをキャッチせずにアプリケーションが落ちてしまっている。

$ node hello2.js
Example app listening on port 3000
/Users/kun432/repository/mac-redis/hello2.js:13
      throw new Error('BROKEN');
      ^

Error: BROKEN
    at Timeout._onTimeout (/Users/kun432/repository/mac-redis/hello2.js:13:13)
    at listOnTimeout (node:internal/timers:559:17)
    at processTimers (node:internal/timers:502:7)

Node.js v17.4.0

Promiseを使ってみる。あんまりPromiseわかってないけど、せっかくなのでsetTimeoutを使ってみた。

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/', (req, res, next) => {
  new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 5000)
  }).then(() => {
    throw new Error('BROKEN');
  }).catch((err) => {
    next(err);
  })
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

もうちょっとシンプルに書いてみる

const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
const morganBody = require('morgan-body')

app.use(express.json())
app.use(express.urlencoded({ extended: true }));
morganBody(app);

app.all('/', (req, res, next) => {
  new Promise(resolve => setTimeout(resolve, 5000)).then(() => {
    throw new Error('BROKEN');
  }).catch(next)
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

Promiseだとエラーとrejectを返すので、catchでは暗黙的にエラーが受け取られて、指定するのはnextだけ、っていう理解。

疲れたのでここまで。

まとめ

expressでアプリを落とさずにエラーを処理させるには、きちんとexpressにエラーを渡す必要がある。