FastifyのヘッダーをHooksで共通化する

問題

Fastifyで書かれたAPIサーバにて、以下のようなルートハンドラがあるとする。

getHelloHandler (request: FastifyRequest, reply: FastifyReply) {
    reply.header('Content-Type', 'application/json').code(200);
    reply.send({ hello: 'world' });
}

上記に出てくるreply.header('Content-Type', 'application/json')は、レスポンスヘッダにContent-Type: application/jsonを追加するコードである。
レスポンスをJSONで返すことが決まっている場合、このように毎回書くのは面倒くさい。これをHooksを使って共通化する。

ソリューション

Fastifyには、リクエスト/レスポンス、およびアプリケーションのライフサイクル*1に準じた複数のイベントトリガが設定されている。

www.fastify.io

これらのイベントトリガが発火したときに実行するフックを定義しておくことで、パケットに対して柔軟な制御ができる。

今回は上記のようなユーザ定義のルートハンドラを実行する前にヘッダを追加しておきたい。そこで、preHandlerフックを使う。

const serer = Fastify();
server.addHook("preHandler", (request, reply, done) => {
      reply.header("Content-Type", "application/json");
      done();
});

これによって、すべてのレスポンスパケットにContent-Type: application/jsonが付与されるようになる。

課題点

以下の場合は実行時に"Reply already sent"なるエラーが発生する。

  1. Content-Typeとしてapplication/jsonまたはtext/plain以外を指定したとき
  2. Content-Typeとしてtext/plainを指定しているにもかかわらず、オブジェクト型をreply.sendの引数に与えたとき

1は、Fastifyがデフォルトでapplication/jsonとtext/plain以外をサポートしていないためである。それ以外のContent-Typeヘッダーを追加したい場合は、以下の方法でパーサーを追加しなければならない。

www.fastify.io

Fastifyはreply.send()の引数のパースを、Content-Typeヘッダーの有無と、その種類を手がかりに行っている*2。したがって、2はtext/plainのパーサーがオブジェクト型を予期しておらず、パースに失敗した時点で処理を停止するのが原因と考えられる。

注意すべきなのは、このエラーで処理が失敗した際には、エラーメッセージのわりに、クライアント側にはレスポンスが返らないことだ。何らかのエラーハンドリングを必要とするが、個人的にはonErrorフックでレスポンスを返すのが良いと思う。

まとめ

FastifyのHooksの一例を紹介した。Fastify自体は理路整然と作ってあるものの、日本語の文献が少なく若干とっつきづらいところがある。今後も便利な機能があれば紹介していきたい。

*1:ライフサイクルはここを参照: https://www.fastify.io/docs/latest/Lifecycle/

*2:lib/reply.jsのReply.prototype.send参照