Expressのerror handlingを理解し、middlewareで実装してみた
はじめに
こんにちは、SHIFT の開発部門に所属しているKatayamaです。今期から転属になり、開発を担当していくことになりました。
現在、基本的な事から学ぶ研修中です。開発部門では新しく学ぶことがたくさんあり、それらを自身の振り返りアウトプットとして発信していけたらと思います。記事が溜まったら、noteのマガジンにもまとめる予定です。
今回はExpressのエラーハンドリングの実装方法について、その実装方法のオプションについてと、middlewareを使った実装方法について理解したので、それについてまとめておこうと思います。
Expressとは
Expressのデフォルトエラーハンドリング
Expressにはデフォルトのエラーハンドリングが実装されており、以下のようなコードを書くとちゃんと500エラーが返ってくる。
const express = require('express');
const app = express();
app.get('/', (req, res) => {
throw new Error('test');
});
app.listen(3030);
$ curl http://localhost:3030/ -i
HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 2154
Date: Tue, 19 Oct 2021 05:56:36 GMT
Connection: keep-alive
Keep-Alive: timeout=5
\<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: test<br> 省略</pre>
</body>
</html>
・参考:Catching Errors
ミドルウェアを作成する
以下でエラーハンドリングを実装する上で理解しておきたい事なのでここで取り上げる。
"Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle. The next function is a function in the Express router which, when invoked, executes the middleware succeeding the current middleware."と公式に書いてあるが、要はreq/resオブジェクトにアクセスして何かしらの処理をする、そして処理を継続させたり継続先にオブジェクトを渡したりできる(継続する処理の種別も選べたりもする)、新しい関数を定義できるという事。
実際にいくつか例を見ていく。
ベースとなるものは以下のコード。
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!')
});
app.listen(3030);
・参考:Writing middleware for use in Express apps
reqプロパティにrequestTimeというミドルウェア関数を追加
// 省略
const requestTime = (req, res, next) => {
req.requestTime = Date.now();
next();
};
app.use(requestTime);
app.get('/', (req, res) => {
res.send(`Hello World! RequestTime is ${req.requestTime}.`)
});
// 省略
$ curl http://localhost:3030/ -i
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 42
ETag: W/"2a-XmQih+4lzmb0r3EXIdZiJZKfp+Y"
Date: Tue, 19 Oct 2021 04:12:15 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Hello World! RequestTime is 1634616735814.
resプロパティにerrorというミドルウェア関数を追加
requestTimeは単なる値だったので、今度は関数をresプロパティに設定してみる。
// 省略
const error = (req, res, next) => {
res.error = () => {
return res.status(404).json({msg: "Not Found"});
};
next();
};
app.use(error);
app.get('/', (req, res) => {
res.error();
});
// 省略
$ curl http://localhost:3030/ -i
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 19
ETag: W/"13-P+qCkklpIor9AGVELBPe43GLeIc"
Date: Tue, 19 Oct 2021 04:10:45 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"msg":"Not Found"}
エラーハンドリング
本題のエラーハンドリングについてみていく。
上記のようにエラーハンドリングはデフォルトで実装されているもの、自分でカスタマイズしたい事もある。以下ではエラーハンドリングの実装方法としてはどんな方法があるのか?を順に考えていきたい。
※ただし、REST APIの実装をExpressで実装した時のことを想定するため、サーバから返るものは全てJSONである事が前提となる。
※REST APIという事でDBからのデータを返すものを想定するが、実際にその実装をするのは面倒なのでmake-errorを使ってエラーが発生したことを想定して実装を考えてみた。DBからのデータ取得の部分は以下のようなイメージ。
const mysql = require("mysql2/promise");
const connection = await mysql.createConnection({
host: "192.168.56.2",
user: "root",
database: "test",
});
app.get("/:id", (req, res) => {
try {
const [rows] = await connection.query(
"SELECT * FROM `hoge` WHERE `id` = ?",
req.params.id
);
const row = rows.shift();
return res.status(200).json(row);
} catch (error) {
// ここのエラーハンドリングの実装を考える
} finally {
connection.release();
}
});
単純にry-catchのcatchでres.json()
単純にやるなら以下のような形で各REST APIのcatchブロックで実装をすることができる。
(なんとなくこのパターンが一番多くみられる気がしているが、これだとエラーの処理を毎回実装するする事になり大変そうでもある)。
const makeError = require("make-error");
function CustomError(status, msg) {
CustomError.super.call(this, msg);
this.status = status;
}
makeError(CustomError);
app.get("/", (req, res) => {
try {
throw new CustomError(404, "Not Found");
} catch (error) {
console.error(error);
return res.status(error.status).json({ msg: error.message });
}
});
app.use((err, req, res, next) => {})でデフォルトのエラーハンドリングの処理を上書き
Expressのデフォルトのエラーハンドリングを上書きして、そこでエラーを処理するようにする、という事もできる。
実装としては以下のようなイメージ。
app.get("/", (req, res, next) => {
try {
throw new CustomError(404, "Not Found");
} catch (error) {
next(error);
}
});
app.use((err, req, res, next) => {
console.error(err);
if (err.status) res.status(err.status);
if (!res.statusCode) res.status(500);
// さらに何か処理があれば書いていくイメージ
res.json({ msg: err.message });
});
$ curl http://localhost:3030/ -i
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 19
ETag: W/"13-P+qCkklpIor9AGVELBPe43GLeIc"
Date: Tue, 19 Oct 2021 08:24:06 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"msg":"Not Found"}
curlの結果を見てもわかるように、ちゃんと意図したとおりに動いている事がわかる。この方法だと共通でエラー処理が書けそうなのでよさそうではある。
ただ、ここにエラーハンドリングの処理を書いていくと冗長なコードになりそうなので(他のミドルウェアの処理でエラーが発生した時の事を考え始めたりすると)、せっかくならどこか別の所にエラーハンドリングの処理を切り出したい。。。
そこでresponse.errorプロパティにミドルウェア関数を定義+default error handlerとの組み合わせのようにしてみるのが1つのやり方としてあり得ると思われる。
response.errorプロパティにミドルウェア関数を定義+default error handlerとの組み合わせ
ミドルウェアを作成するで取り上げた仕組みを応用する。
まずはresponseのプロパティにerrorというミドルウェア関数を追加する。
const error = (req, res, next) => {
res.error = (err) => {
if (err.status) res.status(err.status);
if (!res.statusCode) res.status(500);
// さらに何か処理があれば書いていくイメージ
return res.json({ msg: err.message });
};
next();
};
※if (err.status) res.status(err.status);
エラーが起きた時に常にerrにstatusプロパティがあるとは限らない(何かしらの他のミドルウェアとかでエラーになるとかを想定している)のでこうしている。
※if (!res.statusCode) res.status(500);
本来ならこのコードの上のコード(res.status(err.status))でresponseのstatusCodeプロパティが設定されるはずだが、それがundefined等の場合は、REST APIのエラーステータスが返ってきていないエラーで想定外なので500を設定している。
次にデフォルトエラーハンドラーの処理を上書きして、responseプロパティのerror()関数を使うように設定する。
app.use((err, req, res, next) => {
res.status(err.status || 500).error(err);
});
※err.status || 500
Expressの全てのエラーがここにくるので、errの中にstatusプロパティがない事も想定される。その場合には500を明示的に設定するようにしている。
これで完成。
実際に以下のようなコードで検証してみると・・・
// 省略
const error = (req, res, next) => {
res.error = (err) => {
if (err.status) res.status(err.status);
if (!res.statusCode) res.status(500);
// さらに何か処理があれば書いていくイメージ
return res.json({msg: err.message});
};
next();
};
app.use(error);
app.get("/", (req, res) => {
try {
throw new CustomError(404, "Not Found");
} catch (error) {
return res.status(error.status).error(error);
}
});
app.use((err, req, res, next) => {
res.status(err.status || 500).error(err);
});
// 省略
$ curl http://localhost:3030/ -i
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 19
ETag: W/"13-P+qCkklpIor9AGVELBPe43GLeIc"
Date: Tue, 19 Oct 2021 08:38:51 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"msg":"Not Found"}
動きとしては今まで同様に意図通り。さらにAPIの部分のcatchブロックについても、next()でエラーをデフォルトのエラーハンドリングまで飛ばしてそこで処理ではなく、catchブロック内からresponseが返るように変わるので、なんとなく直感的になったと思われる。
あとはconst errro = () => {}の部分を別のファイルに切り出す(モジュールを作成してrequireする)をすればすっきりさせることができる(Writing middleware for use in Express appsのConfigurable middlewareを参照)。
module.exports = (req, res, next) => {
res.error = (err) => {
// 省略
};
next();
};
const error = require("./error");
app.use(error);
・参考:Writing middleware for use in Express apps
・参考:Using middleware
・参考:Error Handling
・参考:make-error
まとめとして
Expressのエラーハンドリングについて実装方法のオプションを見ていく事で、エラーハンドリングの実装方法にどんな方法があるのか、また、middlewareを使ったエラーハンドリングの実装についても理解を深められた。
おまけ
Expressではapp.use()でミドルウエア(今回のエラーハンドリングの処理など)を設定できるが、"Middleware functions are executed sequentially, therefore the order of middleware inclusion is important."と書かれている通り、その実行順序は定義した順になる(上から順番に実行される)。
参考:Expressのapp.useが記述した順で動かない?
__________________________________
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/