前回の続き。
目次
デフォルトのエラーハンドラー
expressではビルトインのエラーハンドラがついている、というのはどういうことか?前回のコードを再掲。
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}`) })
POSTしてみる。
$ 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> at /Users/kun432/repository/mac-redis/hello2.js:11:9<br> at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)<br> at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)</pre> </body> </html>
このときアプリを起動したコンソールはこうなってる。
$ node hello2.js Example app listening on port 3000 Request: POST / at Sun Feb 20 2022 23:15:16 GMT+0900, IP: ::ffff:127.0.0.1, User Agent: curl/7.77.0 Request Body: { "foo": 1, "bar": 2 } Response: 500 2.173 ms Error: BROKEN at /Users/kun432/repository/mac-redis/hello2.js:11:9 at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14)
デフォルトのエラーハンドラーがあるので、特に何もしていないけどちゃんとエラーとして処理してくれているということ。
ちなみにスタックトレースが表示されているけど、Production環境にすればユーザ側には表示されない。もちろんコンソールには表示される。
$ NODE_ENV=production node hello2.js
$ 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>Internal Server Error</pre> </body> </html>
このデフォルトのエラーハンドラは、ミドルウェアとして実装されており、自分で追加したミドルウェアの一番最後に追加される。このときの動きとしては、
- err.statusかerr.statusCodeから、ステータスコード(res.statusCode)がセットされる
- ステータスコードからステータスメッセージ(res.statusMessage)がセットされる
- Production環境の場合はステータスコードメッセージを含むHTML、そうでない場合はスタックトレースが生成される。
- err.headersで指定されたヘッダーがセットされる
がレスポンスとして返ることになる。ここ、日本語のドキュメントだと書いてないので、英語のドキュメントを読んだほうがいいみたい。
で、明示的に、というか、カスタムなエラーハンドラを自前で書く場合はこうなる。
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.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Internal Server Errorだよ'); }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
ルーティングの後ろに書いているのがミソ。書かなければExpressがもともと持ってるデフォルトエラーハンドラがこの位置にあるようなイメージみたい。
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' Internal Server Errorだよ!
ただし、すでにレスポンスを返している途中でエラーをnextで渡すと(ちょっとこのケースが想定できないのだけど、ドキュメントだとストリームで返している途中、とからしい)はデフォルトのエラーハンドラが呼ばれて、コネクションが切れて、リクエストが失敗する。このケースでは、カスタムなエラーハンドラ内でnextでエラーを渡す必要がある。こんな感じ?
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.use((err, req, res, next) => { if (res.headersSent) { return next(err); // ここのことだと思う } res.status(500) res.json({ error: err }) }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
これちょっとサンプルが思いつかないのでわからない...
エラーハンドラを書く
上で既に書いているけど、エラーハンドラをミドルウェアで書くときは、引数が4つになる。
app.use((err, req, res, next) => { console.error(err.stack) res.status(500).send('Something broke!') })
複数のエラー処理ミドルウェアを並べることもできる。
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); function logErrors (err, req, res, next) { console.error('ERR_LOG:' + err.stack) next(err) } function clientErrorHandler (err, req, res, next) { if (req.xhr) { res.status(500).json({ error: 'Something failed!' }) } else { next(err) } } function errorHandler (err, req, res, next) { res.status(500) res.send('error') } app.all('/', (req, res) => { throw new Error('BROKEN') // Express will catch this on its own. }) app.use(logErrors) app.use(clientErrorHandler) app.use(errorHandler) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
logErrorsがコンソールに出力して、ajaxクライアントならclientErrorHandler、それ以外は最後のキャッチオールなerrorHandlerという感じで処理ができる。試してみる。
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -d '{"foo":1,"bar":2 }' error
これはerrorHandlerのレスポンス。そしてアプリケーション側のコンソールを見てみる。
ERR_LOG:Error: BROKEN at /Users/kun432/repository/mac-redis/hello2.js:29:9 at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) Request: POST / at Mon Feb 21 2022 01:29:18 GMT+0900, IP: ::ffff:127.0.0.1, User Agent: curl/7.77.0 Request Body: { "foo": 1, "bar": 2 } Response Body: error Response: 500 2.519 ms
最初にlogErrorsによるエラー出力が行われてから、errorHandlerのレスポンスが返ってるのがわかる。
ajaxクライアントの場合を擬似的に試してみる。
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -H "X-Requested-With: XMLHttpRequest" -d '{"foo":1,"bar":2 }' {"error":"Something failed!"}
ERR_LOG:Error: BROKEN at /Users/kun432/repository/mac-redis/hello2.js:29:9 at Layer.handle [as handle_request] (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/layer.js:95:5) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:137:13) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) at next (/Users/kun432/repository/mac-redis/node_modules/express/lib/router/route.js:131:14) Request: POST / at Mon Feb 21 2022 01:32:01 GMT+0900, IP: ::ffff:127.0.0.1, User Agent: curl/7.77.0 Request Body: { "foo": 1, "bar": 2 } Response Body: { "error": "Something failed!" } Response: 500 2.966 ms
最初にlogErrorsによるエラー出力が行われてから、clientErrorHandlerのレスポンスが返ってるのがわかる。
エラーだけじゃなくて、複数のミドルウェアがあって、別のルートハンドラがある場合、nextはミドルウェアをスキップさせたり、というようなルーティングもできる。
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.post('/', (req, res, next) => { if (!req.body.user) { next('route'); } else { next(); } }, (req, res, next) => { res.json({"status": "paid"}) } ) app.post('/', (req, res, next) => { res.json({"status": "unpaid"}) } ) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
動かしてみるとこんな感じ。
$ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -H "X-Requested-With: XMLHttpRequest" -d '{"user": "John", "foo": "bar"}' {"status":"paid"} $ curl -X POST 'http://localhost:3000/' -H "Content-type: application/json" -H "X-Requested-With: XMLHttpRequest" -d '{"foo": "bar"}' {"status":"unpaid"}
req.body.userが定義されていなければnext('route)で次のミドルウェアをスキップさせて、後ろのルートハンドラに飛ばしている。
まとめ
わかったような、よくわからないような・・・とりあえず、もう少しいじってみて、自分なりの、ベスト、とは言わないまでも、ベターな書き方を固めたい。