REST APIのカスタムエラーメッセージ処理
1概要
このチュートリアルでは、Spring REST API用のグローバルエラーハンドラの実装方法について説明します。
各例外のセマンティクスを使用して、クライアントに意味のあるエラーメッセージを作成し、問題を簡単に診断するためにそのクライアントにすべての情報を提供することを明確にします。
2カスタムエラーメッセージ
まずは、エラーをネットワーク経由で送信するための単純な構造体
ApiError
を実装することから始めましょう。
public class ApiError {
private HttpStatus status;
private String message;
private List<String> errors;
public ApiError(HttpStatus status, String message, List<String> errors) {
super();
this.status = status;
this.message = message;
this.errors = errors;
}
public ApiError(HttpStatus status, String message, String error) {
super();
this.status = status;
this.message = message;
errors = Arrays.asList(error);
}
}
ここでの情報は簡単なはずです。
-
ステータス
:HTTPステータスコード -
message
:例外に関連したエラーメッセージ -
error
:構築されたエラーメッセージのリスト
そしてもちろん、Springでの実際の例外処理ロジックについては、リンク:/@(Springを使用した例外処理)
@ ControllerAdvice
アノテーションを使用します。
@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
...
}
3不正な要求の例外処理
3.1. 例外処理
それでは、最も一般的なクライアントエラーを処理する方法を見てみましょう – 基本的にクライアントのシナリオが無効なリクエストをAPIに送信した場合:
-
BindException
:この例外は、致命的なバインディングエラーが発生した場合にスローされます。
起こる。
-
MethodArgumentNotValidException
:この例外は、次の場合にスローされます。
@ Valid
でアノテーションが付けられた引数は検証に失敗しました:
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
List<String> errors = new ArrayList<String>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.add(error.getField() + ": " + error.getDefaultMessage());
}
for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD__REQUEST, ex.getLocalizedMessage(), errors);
return handleExceptionInternal(
ex, apiError, headers, apiError.getStatus(), request);
}
ご覧のとおり、**
ResponseEntityExceptionHandler
から基本メソッドをオーバーライドし、独自のカスタム実装を提供しています。
これは必ずしも当てはまるとは限りません。後で説明するように、基本クラスにデフォルトの実装がないカスタム例外を処理する必要がある場合があります。
次:
-
MissingServletRequestPartException
:この例外は以下の場合にスローされます。
マルチパートリクエストの一部が見つからない場合
**
MissingServletRequestParameterException
:この例外はスローされます
リクエストにパラメータがない場合:
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
String error = ex.getParameterName() + " parameter is missing";
ApiError apiError =
new ApiError(HttpStatus.BAD__REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
-
ConstrainViolationException
:この例外は次の結果を報告します。
制約違反:
@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleConstraintViolation(
ConstraintViolationException ex, WebRequest request) {
List<String> errors = new ArrayList<String>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getRootBeanClass().getName() + " " +
violation.getPropertyPath() + ": " + violation.getMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD__REQUEST, ex.getLocalizedMessage(), errors);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
-
TypeMismatchException
:この例外はbeanを設定しようとしたときにスローされます。
間違った型のプロパティ。
-
MethodArgumentTypeMismatchException
:この例外は、次の場合にスローされます。
メソッドの引数が予期された型ではありません。
@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(
MethodArgumentTypeMismatchException ex, WebRequest request) {
String error =
ex.getName() + " should be of type " + ex.getRequiredType().getName();
ApiError apiError =
new ApiError(HttpStatus.BAD__REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
3.2. クライアントからAPIを利用する
それでは、
MethodArgumentTypeMismatchException
に遭遇するテストを見てみましょう。
@Test
public void whenMethodArgumentMismatch__thenBadRequest() {
Response response = givenAuth().get(URL__PREFIX + "/api/foos/ccc");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.BAD__REQUEST, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("should be of type"));
}
そして最後に – これと同じ要求を考えます。
Request method: GET
Request path: http://localhost:8080/spring-security-rest/api/foos/ccc
-
このようなJSONエラーレスポンスは次のようになります。**
{
"status": "BAD__REQUEST",
"message":
"Failed to convert value of type[java.lang.String]
to required type[java.lang.Long]; nested exception
is java.lang.NumberFormatException: For input string: \"ccc\"",
"errors":[ "id should be of type java.lang.Long"
]}
4
NoHandlerFoundException
を処理します.
次に、次のように、404応答を送信する代わりにこの例外をスローするようにサーブレットをカスタマイズできます。
<servlet>
<servlet-name>api</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
その後、これが発生したら、他の例外と同様に処理することができます。
@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
ApiError apiError = new ApiError(HttpStatus.NOT__FOUND, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}
これは簡単なテストです。
@Test
public void whenNoHandlerForHttpRequest__thenNotFound() {
Response response = givenAuth().delete(URL__PREFIX + "/api/xx");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.NOT__FOUND, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("No handler found"));
}
リクエスト全体を見てみましょう。
Request method: DELETE
Request path: http://localhost:8080/spring-security-rest/api/xx
そして
error JSONレスポンス:
{
"status":"NOT__FOUND",
"message":"No handler found for DELETE/spring-security-rest/api/xx",
"errors":[ "No handler found for DELETE/spring-security-rest/api/xx"
]}
5
HttpRequestMethodNotSupportedException
を処理します.
次に、別の興味深い例外、
HttpRequestMethodNotSupportedException
を見てみましょう。これは、サポートされていないHTTPメソッドでリクエストを送信したときに発生します。
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getMethod());
builder.append(
" method is not supported for this request. Supported methods are ");
ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));
ApiError apiError = new ApiError(HttpStatus.METHOD__NOT__ALLOWED,
ex.getLocalizedMessage(), builder.toString());
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
これは、この例外を再現した簡単なテストです。
@Test
public void whenHttpRequestMethodNotSupported__thenMethodNotAllowed() {
Response response = givenAuth().delete(URL__PREFIX + "/api/foos/1");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.METHOD__NOT__ALLOWED, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("Supported methods are"));
}
そして、これが完全な要求です。
Request method: DELETE
Request path: http://localhost:8080/spring-security-rest/api/foos/1
そして
エラーJSON応答:
{
"status":"METHOD__NOT__ALLOWED",
"message":"Request method 'DELETE' not supported",
"errors":[ "DELETE method is not supported for this request. Supported methods are GET "
]}
6.
HttpMediaTypeNotSupportedException
を処理します.
次に、
HttpMediaTypeNotSupportedException
(クライアントがサポートされていないメディアタイプの要求を送信したときに発生します)を次のように処理します。
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
HttpMediaTypeNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getContentType());
builder.append(" media type is not supported. Supported media types are ");
ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));
ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED__MEDIA__TYPE,
ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
これはこの問題に実行中の簡単なテストです:
@Test
public void whenSendInvalidHttpMediaType__thenUnsupportedMediaType() {
Response response = givenAuth().body("").post(URL__PREFIX + "/api/foos");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.UNSUPPORTED__MEDIA__TYPE, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("media type is not supported"));
}
最後に – これがサンプルリクエストです。
Request method: POST
Request path: http://localhost:8080/spring-security-
Headers: Content-Type=text/plain; charset=ISO-8859-1
そして
エラーJSON応答:
{
"status":"UNSUPPORTED__MEDIA__TYPE",
"message":"Content type 'text/plain;charset=ISO-8859-1' not supported",
"errors":["text/plain;charset=ISO-8859-1 media type is not supported.
Supported media types are text/xml
application/x-www-form-urlencoded
application/** +xml
application/json;charset=UTF-8
application/** +json;charset=UTF-8 ** /"
]}
7. デフォルトハンドラ
最後に、フォールバックハンドラ、つまり特定のハンドラを持たない他のすべての例外を処理する包括的なタイプのロジックを実装しましょう。
@ExceptionHandler({ Exception.class })
public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) {
ApiError apiError = new ApiError(
HttpStatus.INTERNAL__SERVER__ERROR, ex.getLocalizedMessage(), "error occurred");
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
8結論
Spring REST API用の適切で成熟したエラーハンドラを構築するのは困難で、間違いなく反復プロセスです。このチュートリアルがあなたのAPIのためにそれをすることへの良い出発点であり、そしてまたあなたがあなたのAPIのクライアントが迅速かつ容易にエラーを診断しそしてそれらを過ぎて移動するのを助ける方法を見るべき良いアンカーとなるでしょう。
このチュートリアルの完全な実装はhttps://github.com/eugenp/tutorials/tree/master/spring-security-rest[the github project]にあります。そのままインポートして実行するのは簡単です。