Spring Bootでは、リクエストのAcceptで指定するメディアタイプによってはカスタムExceptionControllerが当たらない

こんにちは。 エキサイト株式会社の三浦です。

Spring BootにはExceptionControllerというものがあり、それを使うことでControllerが投げる例外の種類によってリクエスト元に返すステータスコードやレスポンスを制御することが出来ます。

このExceptionControllerにはカスタムのものを設定することもできるのですが、特定の条件下ではカスタムのExceptionControllerをうまく使えないときがあります。

今回は、メディアタイプに関連してカスタムのExceptionControllerが当たらない場合について説明します。

ExceptionController

Spring BootにはExceptionControllerというものがあります。

これにはデフォルトのものも存在するのですが、以下のようにカスタムで設定することも出来ます。

@RestControllerAdvice
public class ExceptionController {
    @ExceptionHandler({NotFoundException.class}) // NotFoundException は、独自の例外用クラス
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, String> handleNotFoundException(Exception exception) {
        return Map.of("error", Optional.ofNullable(exception.getMessage()).orElse(""));
    }

    @ExceptionHandler({BadRequestException.class}) // BadRequestException は、独自の例外用クラス
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleBadRequestException(Exception exception) {
        return Map.of("error", Optional.ofNullable(exception.getMessage()).orElse(""));
    }

    @ExceptionHandler()
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, String> handleException(Exception exception) {
        return Map.of("error", Optional.ofNullable(exception.getMessage()).orElse(""));
    }
}

このように設定すると、

  • NotFoundException というクラスが例外として投げられたら、リクエスト元に HttpStatus.NOT_FOUND (404エラー)を返す
  • BadRequestExceptionというクラスが例外として投げられたら、リクエスト元に HttpStatus.BAD_REQUEST (400エラー)を返す
  • それ以外の例外が投げられたら、リクエスト元に HttpStatus.INTERNAL_SERVER_ERROR (500エラー)を返す

というような挙動となります。

実際に、今回の設定で BadRequestException を投げると、以下のようなエラーが返ってきます。

投げられた例外

400エラー

既存・独自両方の例外クラスを設定でき、非常に便利な機能と言えるでしょう。

Acceptするメディアタイプによっては、ExceptionControllerが当たらない

さて、もう一回、全く同じURL・クエリパラメータで投げてみましょう。

投げられた例外

なにやら先程よりログの量が増えていますが、少なくとも BadRequestException が投げられたことはわかります。

レスポンスはどうでしょうか。

500エラー

なんと、500エラーになっています。

実は、URL・クエリパラメータは変えなかったのですが、それ以外で変更した部分がありました。

それはリクエストヘッダーの Accept です。

変更前

変更後

Accepttext/plain のみに制限した結果、レスポンスが変化したのです。

改めて、先程のログを良く見てみましょう。

例外ログ全容

BadRequestException の後に、 ExceptionHandlerExceptionResolverHttpMediaTypeNotAcceptableException というものが続いています。

今回のレスポンスはJSON形式で返ってくるので、 Accept では application/json が許容されている必要があったのですが、 text/plain に限定したことによって「メディアタイプが許容されてない」というエラー( HttpMediaTypeNotAcceptableException )が起きてしまったのです。

一見すると「この場合は、どのエラーにも引っかからなかったパターンである 500エラー を返す挙動になりそう」に見えますが、このケースではどうやらカスタムのExceptionControllerではなくデフォルトのExceptionControllerが使用されることになってしまうようで、レスポンスのステータスコードが想定外のものとなってしまったのでした。

まとめ

多くの場合カスタムなExceptionControllerはとても有用ですが、今回のメディアタイプ非許容など、特定の条件下だと自動的にデフォルトのExceptionControllerが使われてしまうケースも存在します。

メディアタイプ非許容に関しては、例えば外部公開のAPIに対してbot等がアクセスする、などで発生する場面もあるでしょう。

「自動的にデフォルトのExceptionControllerが使われる」場合があることを知らないと、謎のステータスコードの発生に対する調査はかなり大変なはずです。

この記事が、少しでもそういった調査の助けになれば幸いです。