kun432's blog

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

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

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

f:id:kun432:20220213014606p:plain

前回の続き。

目次

デフォルトのエラーハンドラー

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> &nbsp; &nbsp;at /Users/kun432/repository/mac-redis/hello2.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>

このときアプリを起動したコンソールはこうなってる。

$ 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)で次のミドルウェアをスキップさせて、後ろのルートハンドラに飛ばしている。

まとめ

わかったような、よくわからないような・・・とりあえず、もう少しいじってみて、自分なりの、ベスト、とは言わないまでも、ベターな書き方を固めたい。