Spring Boot な Web アプリケーションでエラーレスポンスを確実に JSON 形式で返却したい

極力手軽に実現する方法についてメモしています。

背景

Spring Boot を使った API サーバにおいて、存在しないパスへの HTTP リクエストを受け付けたときの 404 などエラーレスポンスを返す場合に 確実に JSON 形式でレスポンスを返す (すなわち、HTML を返却させない) ことを考えます。

Spring Boot は、Java の OkHttp や Python の Requests といった各種言語の HTTP クライアントライブラリや curl コマンドからの HTTP リクエストであれば、素の設定のままでも以下のようにいい感じに JSON 形式のエラーレスポンスを返すことができます。

{
  "timestamp": "2020-03-24T12:58:57.602+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/no-such-path"
}

一方で Web ブラウザ、より正確には Accept ヘッダに text/html 的な値が設定されている HTTP リクエスト の場合は、Spring Boot が 余計な 気を回すおかげで以下のような HTML レスポンスが返却されることになります。

<html>
<body>
<h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>
<div id='created'>Tue Mar 24 22:03:56 JST 2020</div>
<div>There was an unexpected error (type=Not Found, status=404).</div>
<div>No message available</div>
</body>
</html>

この HTML レスポンスは 然るべきエラービューが存在せずフォールバックが発生した場合にレンダリングされる ものなのですが、API サーバとしてはこれでも不都合はないにせよ、エラーレスポンスが JSON になりえないことがあるのは統一感がなく気持ちが悪いので、JSON に統一したいお気持ちがふつふつと湧いてきます。

そういうわけで、なるべく手間をかけずに簡単に、JSON 形式でエラーレスポンスを返却する方法を探ってみます。

エラーレスポンスを JSON で返すようにする簡単な方法

結論から言うと、Spring Boot には content negotiation の機能が備わっている ので、それを利用して常に Content-Type: application/json な HTTP レスポンスを返却させるようにする、という手段で容易に実現できます。

「常に Content-Type: application/json を返却させる」設定を実現するには、 WebMvcConfigurer インタフェース を用います。

このインタフェースを実装したクラスにて configureContentNegotiation() メソッド をオーバーライドすると ContentNegotiationConfigurer オブジェクト が引数経由で得られるので、これに application/json を常に返す ContentNegotiationStrategy オブジェクトを strategies() メソッド で設定することになります。

application/json を常に返す ContentNegotiationStrategy オブジェクト」は、FixedContentNegotiationStrategy クラス のオブジェクトを生成すれば用意できます。

具体的には以下のコードのようになります。

import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.accept.FixedContentNegotiationStrategy;
import org.springframework.web.servlet.config.annotation.*;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.strategies(
                List.of(new FixedContentNegotiationStrategy(MediaType.APPLICATION_JSON)));
        // configurer.defaultContentType(MediaType.APPLICATION_JSON) だと、結局 Accept: text/html の場合に
        // エラーレスポンスを HTML で返してしまうのでダメ 
    }
}

この方法であれば、Whitelabel やら ErrorController やらをゴニョゴニョせずとも目的を果たすことができます。