shiba-hiro’s 備忘録

技術ブログの皮を被ったただの備忘録

Exception Mapping with Kemal

KemalでException Mapping機能を実現するための方法です。
参照実装をgithubへアップしています。

github.com

Kemal自体については前回記事をご覧ください。

Exception Handlerの必要性

プログラムの実装には、例外の適切な生成およびハンドリングが欠かせません。
通信先のサーバーが落ちている、与えられた値が不正なものだった、実装が不十分でランタイムエラーが発生した、などなど。
正常処理を継続できない場合には、きちんとログを残し、状況に応じたレスポンスを返してあげる必要があります。

もちろん、ビジネス上予期できる分岐に関しては、Exceptionではなくifを用いて記述する方法もあります。
そうはいっても、「例外的な内容を記述するif」よりもカスタムしたExceptionの方が素直に状況を表せるケースも多い(Validationなど)ですし、Exceptionの伝達性には実装上の便利な面もあります。

ことWebアプリにおいても例外のハンドリングは重要で、適切なロギングやレスポンスは、障害の察知やユーザビリティにメリットをもたらします。
特定の例外に対して、レベルを上げたログを必ず出力するようにすれば、障害の検知精度を上げられます。
Web APIの処理中にエラーが起きた際、その種類によってHTTPステータスコードや返却メッセージを適切に選んで返せれば、呼び出し側も状況に応じた行動が可能です。

一方、例外処理は、プログラム読みづらくすることもあります。
たとえば次のようなコードがあったとします。

.
.
.
  begin
    changeset = Repo.insert(note)
  rescue e : FirstSampleException
    halt env, status_code: 400, response: Hash{"message" => "Message for FirstSample."}.to_json
  rescue e : SecondSampleException
    halt env, status_code: 422, response: Hash{"message" => "Message for SecondSample"}.to_json
  rescue e : Exception
    LOGGER.error(e.inspect_with_backtrace)
    halt env, status_code: 500, response: Hash{"message" => "Internal server error."}.to_json
  end
.
.
.

プログラムの意味的に重要なのは、noteという値をデータベースへ保存することです。

しかしながら、DBサーバーで障害が発生している等、処理が失敗する可能性もあるでしょう。
そのため例外処理を記述するのですが、 この例外処理は「値をデータベースへ保存する」という意味を表すのには必要ないので、プログラムの見通しを悪くしてしまいます。

尤も、例外処理についての上記のデメリットは、メソッド化する等の方法で対処することもできます。
ただし、この例ではもう一点、問題になりえる点があります。
それは、プログラムの階層を壊してしまう可能性があることです。
プログラムの規模が大きくなると、階層的に、web apiのレスポンスを返す層と、データベースへ値を保存するための層は分けて記述することが多いはずです。
この例のような書き方を放置すると、ゆくゆくはプログラムの階層がカオスになってしまいます。

そこで、特定の例外に対する処理を請け負うレイヤーを分けることで、ビジネスロジック内での例外処理記述を減らす方法で問題の解決を図ります。
これがException Handlerが必要になる理由です。

web apiのリクエスト/レスポンスをやりとりする層を外側、コアになるビジネスロジックやDBアクセスが配置される層を内側と表現すると、Exception Handlerは、比較的外側へ配置されるイメージです。
Exception Handlerは、ビジネスロジックなどから投げられキャッチされなかった例外を、最終的に受けて、処理する役割を果たします。

実装紹介

Exceptionに応じた処理をまとめて記述するための構造がWebアプリに有用なことはわかりましたが、実はKemalには(まだ)これをサポートする機能がありません。
そこで、今回、KemalのCustom Handlerとして記述できるException Handler(Mapper)のサンプルを作成しました。

./exception_handler/以下にマッピングさせたい処理を記述し、こちらのexception_handler.crを読み込ませるだけでOKです。
処理は次のように記述できます。

exception SampleException do |env, ex|
  LOGGER.info "SampleException is mapped."
  LOGGER.error ex.inspect_with_backtrace
  env.response.content_type = "application/json"
  halt env, status_code: 500, response: Hash{"message" => "Internal Server Error", "cause" => "SampleException"}.to_json
end

この例では、ロジック内でSampleExceptionが投げられ、キャッチされなかった場合に、ここで記述した処理が実行されます。
この仕組みにより、ビジネスロジックでは例外処理の記述を減らし、プログラムの見通しをすっきりさせることができるようになります。

雑記

先日Kemalのgitterでこちらのexception-handlerを紹介したところ、そこそこウケたので、ブログでも共有させてもらいました。
将来的にKemal自体に組み込まれてしまえば不要になりますが、それまでは有用な実装になると思います。