This is an automated email from the ASF dual-hosted git repository.
apkhmv pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 2d4a7eb423 IGNITE-19723 Enhance REST error handling (#2826)
2d4a7eb423 is described below
commit 2d4a7eb423a6031c8e7dd268415ec928cbef0b32
Author: Ivan Gagarkin <[email protected]>
AuthorDate: Fri Nov 10 19:45:47 2023 +0700
IGNITE-19723 Enhance REST error handling (#2826)
- Introduced a controller for general errors
- Customized error handlers to replace default Micronaut implementations
- Sets application/problem+json content type where applicable
---
modules/rest-api/build.gradle | 9 +-
.../internal/rest/api/GeneralErrorsController.java | 77 ++++++
.../ignite/internal/rest/constants/HttpCode.java | 206 +++++++++++++-
.../ClusterNotInitializedExceptionHandler.java | 4 +-
.../exception/handler/IgniteExceptionHandler.java | 2 +-
.../IgniteInternalCheckedExceptionHandler.java | 4 +-
.../handler/IgniteInternalExceptionHandler.java | 4 +-
.../exception/handler/JavaExceptionHandler.java | 2 +-
.../AuthenticationExceptionHandlerReplacement.java | 5 +-
.../ConstraintExceptionHandlerReplacement.java | 88 ++++++
.../ContentLengthExceededHandlerReplacement.java} | 21 +-
.../ConversionErrorHandlerReplacement.java | 9 +-
...ltAuthorizationExceptionHandlerReplacement.java | 5 +-
.../replacement/HttpStatusHandlerReplacement.java} | 20 +-
.../JsonExceptionHandlerReplacement.java} | 21 +-
.../UnsatisfiedArgumentHandlerReplacement.java} | 21 +-
.../UnsatisfiedRouteHandlerReplacement.java} | 20 +-
.../replacement/UriSyntaxHandlerReplacement.java} | 21 +-
.../internal/rest/problem/HttpProblemResponse.java | 7 +-
...blemResponse.java => ProblemJsonMediaType.java} | 26 +-
.../rest/problem/ProblemJsonMediaTypeCodec.java | 80 ++++++
.../rest/exception/handler/EchoMessage.java | 38 +++
.../rest/exception/handler/ErrorHandlingTest.java | 306 +++++++++++++++++++++
.../rest/exception/handler/TestController.java | 65 +++++
.../rest/exception/handler/ThrowableProvider.java | 30 ++
.../cluster/ItClusterManagementControllerTest.java | 4 +-
26 files changed, 979 insertions(+), 116 deletions(-)
diff --git a/modules/rest-api/build.gradle b/modules/rest-api/build.gradle
index c771a5abeb..1a6c7da775 100644
--- a/modules/rest-api/build.gradle
+++ b/modules/rest-api/build.gradle
@@ -39,10 +39,17 @@ dependencies {
implementation libs.micronaut.security
implementation libs.micronaut.security.annotations
+ testAnnotationProcessor libs.micronaut.inject.annotation.processor
+
testImplementation testFixtures(project(':ignite-core'))
testImplementation libs.junit5.api
- testImplementation libs.mockito.core
testImplementation libs.junit5.params
+ testImplementation libs.mockito.core
+ testImplementation libs.micronaut.junit5
+ testImplementation libs.micronaut.http.client
+ testImplementation libs.micronaut.http.server.netty
+ testImplementation libs.hamcrest.core
+ testImplementation libs.hamcrest.optional
}
compileJava {
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/GeneralErrorsController.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/GeneralErrorsController.java
new file mode 100644
index 0000000000..56c5a1e457
--- /dev/null
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/GeneralErrorsController.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.api;
+
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.HttpStatus;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Error;
+import org.apache.ignite.internal.rest.constants.HttpCode;
+import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
+
+/**
+ * Controller that handles general errors.
+ */
+@Controller
+public class GeneralErrorsController {
+ /**
+ * 404 Not Found.
+ */
+ @Error(status = HttpStatus.NOT_FOUND, global = true)
+ public HttpResponse<? extends Problem> notFound(HttpRequest<?> request) {
+ return HttpProblemResponse.from(
+ Problem.fromHttpCode(HttpCode.NOT_FOUND)
+ .detail("Requested resource not found: " +
request.getPath())
+ );
+ }
+
+ /**
+ * 405 Method Not Allowed.
+ */
+ @Error(status = HttpStatus.METHOD_NOT_ALLOWED, global = true)
+ public HttpResponse<? extends Problem> methodNotAllowed(HttpRequest<?>
request) {
+ return HttpProblemResponse.from(
+ Problem.fromHttpCode(HttpCode.METHOD_NOT_ALLOWED)
+ .detail("Method not allowed: " +
request.getMethodName())
+ );
+ }
+
+ /**
+ * 415 Unsupported Media Type.
+ */
+ @Error(status = HttpStatus.UNSUPPORTED_MEDIA_TYPE, global = true)
+ public HttpResponse<? extends Problem> unsupportedMediaType(HttpRequest<?>
request) {
+ return HttpProblemResponse.from(
+ Problem.fromHttpCode(HttpCode.UNSUPPORTED_MEDIA_TYPE)
+ .detail("Unsupported media type: " +
request.getContentType().map(MediaType::getType).orElse(null))
+ );
+ }
+
+ /**
+ * 522 Connection timed out.
+ */
+ @Error(status = HttpStatus.CONNECTION_TIMED_OUT, global = true)
+ public HttpResponse<? extends Problem> connectionTimedOut(HttpRequest<?>
request) {
+ return HttpProblemResponse.from(
+ Problem.fromHttpCode(HttpCode.CONNECTION_TIMED_OUT)
+ .detail("Connection timed out: " + request.getPath())
+ );
+ }
+}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java
index 18771d74c5..6e53d35100 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/constants/HttpCode.java
@@ -21,14 +21,73 @@ package org.apache.ignite.internal.rest.constants;
* Represents http codes that can be returned by Ignite.
*/
public enum HttpCode {
- OK(200, "OK"),
+ CONTINUE(100, "Continue"),
+ SWITCHING_PROTOCOLS(101, "Switching Protocols"),
+ PROCESSING(102, "Processing"),
+ OK(200, "Ok"),
+ CREATED(201, "Created"),
+ ACCEPTED(202, "Accepted"),
+ NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"),
+ NO_CONTENT(204, "No Content"),
+ RESET_CONTENT(205, "Reset Content"),
+ PARTIAL_CONTENT(206, "Partial Content"),
+ MULTI_STATUS(207, "Multi Status"),
+ ALREADY_IMPORTED(208, "Already imported"),
+ IM_USED(226, "IM Used"),
+ MULTIPLE_CHOICES(300, "Multiple Choices"),
+ MOVED_PERMANENTLY(301, "Moved Permanently"),
+ FOUND(302, "Found"),
+ SEE_OTHER(303, "See Other"),
+ NOT_MODIFIED(304, "Not Modified"),
+ USE_PROXY(305, "Use Proxy"),
+ SWITCH_PROXY(306, "Switch Proxy"),
+ TEMPORARY_REDIRECT(307, "Temporary Redirect"),
+ PERMANENT_REDIRECT(308, "Permanent Redirect"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
+ PAYMENT_REQUIRED(402, "Payment Required"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
- // May be used in case of "Already exists" problem.
+ METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
+ NOT_ACCEPTABLE(406, "Not Acceptable"),
+ PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
+ REQUEST_TIMEOUT(408, "Request Timeout"),
CONFLICT(409, "Conflict"),
- INTERNAL_ERROR(500, "Internal Server Error");
+ GONE(410, "Gone"),
+ LENGTH_REQUIRED(411, "Length Required"),
+ PRECONDITION_FAILED(412, "Precondition Failed"),
+ REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"),
+ REQUEST_URI_TOO_LONG(414, "Request-URI Too Long"),
+ UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
+ REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
+ EXPECTATION_FAILED(417, "Expectation Failed"),
+ I_AM_A_TEAPOT(418, "I am a teapot"),
+ ENHANCE_YOUR_CALM(420, "Enhance your calm"),
+ UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"),
+ LOCKED(423, "Locked"),
+ FAILED_DEPENDENCY(424, "Failed Dependency"),
+ TOO_EARLY(425, "Too Early"),
+ UPGRADE_REQUIRED(426, "Upgrade Required"),
+ PRECONDITION_REQUIRED(428, "Precondition Required"),
+ TOO_MANY_REQUESTS(429, "Too Many Requests"),
+ REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"),
+ NO_RESPONSE(444, "No Response"),
+ BLOCKED_BY_WINDOWS_PARENTAL_CONTROLS(450, "Blocked by Windows Parental
Controls"),
+ UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"),
+ REQUEST_HEADER_TOO_LARGE(494, "Request Header Too Large"),
+ INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
+ NOT_IMPLEMENTED(501, "Not Implemented"),
+ BAD_GATEWAY(502, "Bad Gateway"),
+ SERVICE_UNAVAILABLE(503, "Service Unavailable"),
+ GATEWAY_TIMEOUT(504, "Gateway Timeout"),
+ HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version Not Supported"),
+ VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates"),
+ INSUFFICIENT_STORAGE(507, "Insufficient Storage"),
+ LOOP_DETECTED(508, "Loop Detected"),
+ BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded"),
+ NOT_EXTENDED(510, "Not Extended"),
+ NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"),
+ CONNECTION_TIMED_OUT(522, "Connection Timed Out");
private final int code;
@@ -52,11 +111,142 @@ public enum HttpCode {
*/
public static HttpCode valueOf(int code) {
switch (code) {
- case 200: return OK;
- case 400: return BAD_REQUEST;
- case 404: return NOT_FOUND;
- case 500: return INTERNAL_ERROR;
- default: throw new IllegalArgumentException(code + " is unknown
http code");
+ case 100:
+ return CONTINUE;
+ case 101:
+ return SWITCHING_PROTOCOLS;
+ case 102:
+ return PROCESSING;
+ case 200:
+ return OK;
+ case 201:
+ return CREATED;
+ case 202:
+ return ACCEPTED;
+ case 203:
+ return NON_AUTHORITATIVE_INFORMATION;
+ case 204:
+ return NO_CONTENT;
+ case 205:
+ return RESET_CONTENT;
+ case 206:
+ return PARTIAL_CONTENT;
+ case 207:
+ return MULTI_STATUS;
+ case 208:
+ return ALREADY_IMPORTED;
+ case 226:
+ return IM_USED;
+ case 300:
+ return MULTIPLE_CHOICES;
+ case 301:
+ return MOVED_PERMANENTLY;
+ case 302:
+ return FOUND;
+ case 303:
+ return SEE_OTHER;
+ case 304:
+ return NOT_MODIFIED;
+ case 305:
+ return USE_PROXY;
+ case 306:
+ return SWITCH_PROXY;
+ case 307:
+ return TEMPORARY_REDIRECT;
+ case 308:
+ return PERMANENT_REDIRECT;
+ case 400:
+ return BAD_REQUEST;
+ case 401:
+ return UNAUTHORIZED;
+ case 402:
+ return PAYMENT_REQUIRED;
+ case 403:
+ return FORBIDDEN;
+ case 404:
+ return NOT_FOUND;
+ case 405:
+ return METHOD_NOT_ALLOWED;
+ case 406:
+ return NOT_ACCEPTABLE;
+ case 407:
+ return PROXY_AUTHENTICATION_REQUIRED;
+ case 408:
+ return REQUEST_TIMEOUT;
+ case 409:
+ return CONFLICT;
+ case 410:
+ return GONE;
+ case 411:
+ return LENGTH_REQUIRED;
+ case 412:
+ return PRECONDITION_FAILED;
+ case 413:
+ return REQUEST_ENTITY_TOO_LARGE;
+ case 414:
+ return REQUEST_URI_TOO_LONG;
+ case 415:
+ return UNSUPPORTED_MEDIA_TYPE;
+ case 416:
+ return REQUESTED_RANGE_NOT_SATISFIABLE;
+ case 417:
+ return EXPECTATION_FAILED;
+ case 418:
+ return I_AM_A_TEAPOT;
+ case 420:
+ return ENHANCE_YOUR_CALM;
+ case 422:
+ return UNPROCESSABLE_ENTITY;
+ case 423:
+ return LOCKED;
+ case 424:
+ return FAILED_DEPENDENCY;
+ case 425:
+ return TOO_EARLY;
+ case 426:
+ return UPGRADE_REQUIRED;
+ case 428:
+ return PRECONDITION_REQUIRED;
+ case 429:
+ return TOO_MANY_REQUESTS;
+ case 431:
+ return REQUEST_HEADER_FIELDS_TOO_LARGE;
+ case 444:
+ return NO_RESPONSE;
+ case 450:
+ return BLOCKED_BY_WINDOWS_PARENTAL_CONTROLS;
+ case 451:
+ return UNAVAILABLE_FOR_LEGAL_REASONS;
+ case 494:
+ return REQUEST_HEADER_TOO_LARGE;
+ case 500:
+ return INTERNAL_SERVER_ERROR;
+ case 501:
+ return NOT_IMPLEMENTED;
+ case 502:
+ return BAD_GATEWAY;
+ case 503:
+ return SERVICE_UNAVAILABLE;
+ case 504:
+ return GATEWAY_TIMEOUT;
+ case 505:
+ return HTTP_VERSION_NOT_SUPPORTED;
+ case 506:
+ return VARIANT_ALSO_NEGOTIATES;
+ case 507:
+ return INSUFFICIENT_STORAGE;
+ case 508:
+ return LOOP_DETECTED;
+ case 509:
+ return BANDWIDTH_LIMIT_EXCEEDED;
+ case 510:
+ return NOT_EXTENDED;
+ case 511:
+ return NETWORK_AUTHENTICATION_REQUIRED;
+ case 522:
+ return CONNECTION_TIMED_OUT;
+ default:
+ throw new IllegalArgumentException("Invalid HTTP status code:
" + code);
}
}
}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ClusterNotInitializedExceptionHandler.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ClusterNotInitializedExceptionHandler.java
index 049dc057c5..471594d5a6 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ClusterNotInitializedExceptionHandler.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ClusterNotInitializedExceptionHandler.java
@@ -34,11 +34,11 @@ import
org.apache.ignite.internal.rest.problem.HttpProblemResponse;
@Requires(classes = {ClusterNotInitializedException.class,
ExceptionHandler.class})
public class ClusterNotInitializedExceptionHandler implements
ExceptionHandler<ClusterNotInitializedException, HttpResponse<?
extends Problem>> {
-
@Override
public HttpResponse<? extends Problem> handle(HttpRequest request,
ClusterNotInitializedException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.NOT_FOUND)
+ Problem.fromHttpCode(HttpCode.CONFLICT)
+ .title("Cluster not initialized")
.detail("Cluster not initialized. Call
/management/v1/cluster/init in order to initialize cluster")
);
}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteExceptionHandler.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteExceptionHandler.java
index 4147ac0ac5..a9e6b47322 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteExceptionHandler.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteExceptionHandler.java
@@ -63,7 +63,7 @@ public class IgniteExceptionHandler implements
ExceptionHandler<IgniteException,
}
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.INTERNAL_ERROR)
+ Problem.fromHttpCode(HttpCode.INTERNAL_SERVER_ERROR)
.detail(detail)
.traceId(exception.traceId())
.code(exception.codeAsString())
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/IgniteInternalCheckedExceptionHandler.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteInternalCheckedExceptionHandler.java
similarity index 93%
rename from
modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/IgniteInternalCheckedExceptionHandler.java
rename to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteInternalCheckedExceptionHandler.java
index 85812e73b7..308ba324c3 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/IgniteInternalCheckedExceptionHandler.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteInternalCheckedExceptionHandler.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.cluster.exception.handler;
+package org.apache.ignite.internal.rest.exception.handler;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
@@ -38,7 +38,7 @@ public class IgniteInternalCheckedExceptionHandler
@Override
public HttpResponse<? extends Problem> handle(HttpRequest request,
IgniteInternalCheckedException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.INTERNAL_ERROR)
+ Problem.fromHttpCode(HttpCode.INTERNAL_SERVER_ERROR)
.traceId(exception.traceId())
.code(exception.codeAsString())
.detail(exception.getMessage())
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/IgniteInternalExceptionHandler.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteInternalExceptionHandler.java
similarity index 93%
rename from
modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/IgniteInternalExceptionHandler.java
rename to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteInternalExceptionHandler.java
index 212ef4e6d1..6937a348c2 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/exception/handler/IgniteInternalExceptionHandler.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/IgniteInternalExceptionHandler.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.cluster.exception.handler;
+package org.apache.ignite.internal.rest.exception.handler;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
@@ -37,7 +37,7 @@ public class IgniteInternalExceptionHandler implements
ExceptionHandler<IgniteIn
@Override
public HttpResponse<? extends Problem> handle(HttpRequest request,
IgniteInternalException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.INTERNAL_ERROR)
+ Problem.fromHttpCode(HttpCode.INTERNAL_SERVER_ERROR)
.traceId(exception.traceId())
.code(exception.codeAsString())
.detail(exception.getMessage())
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/JavaExceptionHandler.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/JavaExceptionHandler.java
index dbe4f305e0..e60ee1858b 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/JavaExceptionHandler.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/JavaExceptionHandler.java
@@ -41,7 +41,7 @@ public class JavaExceptionHandler implements
ExceptionHandler<Exception, HttpRes
public HttpResponse<? extends Problem> handle(HttpRequest request,
Exception exception) {
LOG.error("Unhandled exception", exception);
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.INTERNAL_ERROR)
+ Problem.fromHttpCode(HttpCode.INTERNAL_SERVER_ERROR)
.detail(exception.getMessage())
);
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/AuthenticationExceptionHandlerReplacement.java
similarity index 93%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
copy to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/AuthenticationExceptionHandlerReplacement.java
index 094c630415..9a54e7b0c4 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/AuthenticationExceptionHandlerReplacement.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.authentication.exception;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
@@ -34,10 +34,9 @@ import
org.apache.ignite.internal.rest.problem.HttpProblemResponse;
*/
@Singleton
@Replaces(AuthenticationExceptionHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
+@Requires(classes = {AuthenticationException.class, ExceptionHandler.class})
public class AuthenticationExceptionHandlerReplacement implements
ExceptionHandler<AuthenticationException, HttpResponse<? extends
Problem>> {
-
@Override
public HttpResponse<? extends Problem> handle(HttpRequest request,
AuthenticationException exception) {
return HttpProblemResponse.from(
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ConstraintExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ConstraintExceptionHandlerReplacement.java
new file mode 100644
index 0000000000..c0e21b3dea
--- /dev/null
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ConstraintExceptionHandlerReplacement.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.exception.handler.replacement;
+
+import io.micronaut.context.annotation.Replaces;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.server.exceptions.ExceptionHandler;
+import io.micronaut.validation.exceptions.ConstraintExceptionHandler;
+import jakarta.inject.Singleton;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.ElementKind;
+import javax.validation.Path;
+import javax.validation.Path.Node;
+import org.apache.ignite.internal.rest.api.InvalidParam;
+import org.apache.ignite.internal.rest.api.Problem;
+import org.apache.ignite.internal.rest.constants.HttpCode;
+import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
+
+/**
+ * Replacement for {@link ConstraintExceptionHandler}. Returns {@link
HttpProblemResponse}.
+ */
+@Singleton
+@Replaces(ConstraintExceptionHandler.class)
+@Requires(classes = {ConstraintViolationException.class,
ExceptionHandler.class})
+public class ConstraintExceptionHandlerReplacement implements
ExceptionHandler<ConstraintViolationException, HttpResponse<?>> {
+ @Override
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
ConstraintViolationException exception) {
+ Set<InvalidParam> invalidParams = exception.getConstraintViolations()
+ .stream()
+ .map(it -> new InvalidParam(it.getPropertyPath().toString(),
buildMessage(it)))
+ .collect(Collectors.toSet());
+
+ return HttpProblemResponse.from(
+ Problem.fromHttpCode(HttpCode.BAD_REQUEST)
+ .detail("Validation failed")
+ .invalidParams(invalidParams)
+ );
+ }
+
+ private static String buildMessage(ConstraintViolation<?> violation) {
+ Path propertyPath = violation.getPropertyPath();
+ StringBuilder message = new StringBuilder();
+ Iterator<Node> i = propertyPath.iterator();
+
+ while (i.hasNext()) {
+ Path.Node node = i.next();
+
+ if (node.getKind() == ElementKind.METHOD || node.getKind() ==
ElementKind.CONSTRUCTOR) {
+ continue;
+ }
+
+ message.append(node.getName());
+
+ if (node.getIndex() != null) {
+ message.append(String.format("[%d]", node.getIndex()));
+ }
+
+ if (i.hasNext()) {
+ message.append('.');
+ }
+ }
+
+ message.append(": ").append(violation.getMessage());
+
+ return message.toString();
+ }
+}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ContentLengthExceededHandlerReplacement.java
similarity index 67%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
copy to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ContentLengthExceededHandlerReplacement.java
index 094c630415..e5ef7ab02c 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ContentLengthExceededHandlerReplacement.java
@@ -15,33 +15,32 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.authentication.exception;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
+import io.micronaut.http.exceptions.ContentLengthExceededException;
+import io.micronaut.http.server.exceptions.ContentLengthExceededHandler;
import io.micronaut.http.server.exceptions.ExceptionHandler;
-import io.micronaut.security.authentication.AuthenticationException;
-import io.micronaut.security.authentication.AuthenticationExceptionHandler;
import jakarta.inject.Singleton;
import org.apache.ignite.internal.rest.api.Problem;
import org.apache.ignite.internal.rest.constants.HttpCode;
import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
/**
- * Replacement for {@link AuthenticationExceptionHandler}. Returns {@link
HttpProblemResponse}.
+ * Replacement for {@link ContentLengthExceededHandler}. Returns {@link
HttpProblemResponse}.
*/
@Singleton
-@Replaces(AuthenticationExceptionHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
-public class AuthenticationExceptionHandlerReplacement implements
- ExceptionHandler<AuthenticationException, HttpResponse<? extends
Problem>> {
-
+@Replaces(ContentLengthExceededHandler.class)
+@Requires(classes = {ContentLengthExceededException.class,
ExceptionHandler.class})
+public class ContentLengthExceededHandlerReplacement implements
+ ExceptionHandler<ContentLengthExceededException, HttpResponse<?
extends Problem>> {
@Override
- public HttpResponse<? extends Problem> handle(HttpRequest request,
AuthenticationException exception) {
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
ContentLengthExceededException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.UNAUTHORIZED)
+ Problem.fromHttpCode(HttpCode.REQUEST_ENTITY_TOO_LARGE)
.detail(exception.getMessage())
);
}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ConversionErrorHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ConversionErrorHandlerReplacement.java
similarity index 88%
copy from
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ConversionErrorHandlerReplacement.java
copy to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ConversionErrorHandlerReplacement.java
index 1007e313d0..4aa87ca7e4 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ConversionErrorHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/ConversionErrorHandlerReplacement.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.exception.handler;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
@@ -28,21 +28,20 @@ import jakarta.inject.Singleton;
import org.apache.ignite.internal.rest.api.Problem;
import org.apache.ignite.internal.rest.constants.HttpCode;
import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
-import org.apache.ignite.internal.util.ExceptionUtils;
/**
* Replacement for {@link ConversionErrorHandler}. Returns {@link
HttpProblemResponse}.
*/
@Singleton
@Replaces(ConversionErrorHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
+@Requires(classes = {ConversionErrorException.class, ExceptionHandler.class})
public class ConversionErrorHandlerReplacement implements
ExceptionHandler<ConversionErrorException, HttpResponse<? extends Problem>> {
-
@Override
public HttpResponse<? extends Problem> handle(HttpRequest request,
ConversionErrorException exception) {
return HttpProblemResponse.from(
Problem.fromHttpCode(HttpCode.BAD_REQUEST)
-
.detail(ExceptionUtils.getCause(exception).getMessage())
+ .title("Invalid parameter")
+ .detail(exception.getMessage())
);
}
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/DefaultAuthorizationExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/DefaultAuthorizationExceptionHandlerReplacement.java
similarity index 93%
rename from
modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/DefaultAuthorizationExceptionHandlerReplacement.java
rename to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/DefaultAuthorizationExceptionHandlerReplacement.java
index 202014d6f9..701bf5fabc 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/DefaultAuthorizationExceptionHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/DefaultAuthorizationExceptionHandlerReplacement.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.authentication.exception;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
@@ -34,9 +34,8 @@ import
org.apache.ignite.internal.rest.problem.HttpProblemResponse;
*/
@Singleton
@Replaces(DefaultAuthorizationExceptionHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
+@Requires(classes = {AuthorizationException.class, ExceptionHandler.class})
public class DefaultAuthorizationExceptionHandlerReplacement extends
DefaultAuthorizationExceptionHandler {
-
@Override
protected MutableHttpResponse<? extends Problem>
httpResponseWithStatus(HttpRequest<?> request, AuthorizationException
exception) {
return HttpProblemResponse.from(
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/HttpStatusHandlerReplacement.java
similarity index 68%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
copy to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/HttpStatusHandlerReplacement.java
index 094c630415..8fd53bad09 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/HttpStatusHandlerReplacement.java
@@ -15,33 +15,31 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.authentication.exception;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
+import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.http.server.exceptions.ExceptionHandler;
-import io.micronaut.security.authentication.AuthenticationException;
-import io.micronaut.security.authentication.AuthenticationExceptionHandler;
+import io.micronaut.http.server.exceptions.HttpStatusHandler;
import jakarta.inject.Singleton;
import org.apache.ignite.internal.rest.api.Problem;
import org.apache.ignite.internal.rest.constants.HttpCode;
import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
/**
- * Replacement for {@link AuthenticationExceptionHandler}. Returns {@link
HttpProblemResponse}.
+ * Replacement for {@link HttpStatusHandler} that returns {@link Problem}
instead of {@link HttpResponse}.
*/
@Singleton
-@Replaces(AuthenticationExceptionHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
-public class AuthenticationExceptionHandlerReplacement implements
- ExceptionHandler<AuthenticationException, HttpResponse<? extends
Problem>> {
-
+@Replaces(HttpStatusHandler.class)
+@Requires(classes = {HttpStatusException.class, ExceptionHandler.class})
+public class HttpStatusHandlerReplacement implements
ExceptionHandler<HttpStatusException, HttpResponse<? extends Problem>> {
@Override
- public HttpResponse<? extends Problem> handle(HttpRequest request,
AuthenticationException exception) {
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
HttpStatusException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.UNAUTHORIZED)
+
Problem.fromHttpCode(HttpCode.valueOf(exception.getStatus().getCode()))
.detail(exception.getMessage())
);
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/JsonExceptionHandlerReplacement.java
similarity index 68%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
copy to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/JsonExceptionHandlerReplacement.java
index 094c630415..4ace796374 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/JsonExceptionHandlerReplacement.java
@@ -15,33 +15,32 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.authentication.exception;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
+import com.fasterxml.jackson.core.JsonProcessingException;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.server.exceptions.ExceptionHandler;
-import io.micronaut.security.authentication.AuthenticationException;
-import io.micronaut.security.authentication.AuthenticationExceptionHandler;
+import io.micronaut.http.server.exceptions.JsonExceptionHandler;
import jakarta.inject.Singleton;
import org.apache.ignite.internal.rest.api.Problem;
import org.apache.ignite.internal.rest.constants.HttpCode;
import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
/**
- * Replacement for {@link AuthenticationExceptionHandler}. Returns {@link
HttpProblemResponse}.
+ * Replacement for {@link JsonExceptionHandler}. Returns {@link
HttpProblemResponse}.
*/
@Singleton
-@Replaces(AuthenticationExceptionHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
-public class AuthenticationExceptionHandlerReplacement implements
- ExceptionHandler<AuthenticationException, HttpResponse<? extends
Problem>> {
-
+@Replaces(JsonExceptionHandler.class)
+@Requires(classes = {JsonProcessingException.class, ExceptionHandler.class})
+public class JsonExceptionHandlerReplacement implements
ExceptionHandler<JsonProcessingException, HttpResponse<? extends Problem>> {
@Override
- public HttpResponse<? extends Problem> handle(HttpRequest request,
AuthenticationException exception) {
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
JsonProcessingException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.UNAUTHORIZED)
+ Problem.fromHttpCode(HttpCode.BAD_REQUEST)
+ .title("Invalid JSON")
.detail(exception.getMessage())
);
}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ConversionErrorHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UnsatisfiedArgumentHandlerReplacement.java
similarity index 67%
rename from
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ConversionErrorHandlerReplacement.java
rename to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UnsatisfiedArgumentHandlerReplacement.java
index 1007e313d0..0f29364de2 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ConversionErrorHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UnsatisfiedArgumentHandlerReplacement.java
@@ -15,34 +15,33 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.exception.handler;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
-import io.micronaut.core.convert.exceptions.ConversionErrorException;
+import io.micronaut.core.bind.exceptions.UnsatisfiedArgumentException;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
-import io.micronaut.http.server.exceptions.ConversionErrorHandler;
import io.micronaut.http.server.exceptions.ExceptionHandler;
+import io.micronaut.http.server.exceptions.UnsatisfiedArgumentHandler;
import jakarta.inject.Singleton;
import org.apache.ignite.internal.rest.api.Problem;
import org.apache.ignite.internal.rest.constants.HttpCode;
import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
-import org.apache.ignite.internal.util.ExceptionUtils;
/**
- * Replacement for {@link ConversionErrorHandler}. Returns {@link
HttpProblemResponse}.
+ * Replacement for {@link UnsatisfiedArgumentHandler} that returns {@link
Problem} instead of {@link HttpResponse}.
*/
@Singleton
-@Replaces(ConversionErrorHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
-public class ConversionErrorHandlerReplacement implements
ExceptionHandler<ConversionErrorException, HttpResponse<? extends Problem>> {
-
+@Replaces(UnsatisfiedArgumentHandler.class)
+@Requires(classes = {UnsatisfiedArgumentException.class,
ExceptionHandler.class})
+public class UnsatisfiedArgumentHandlerReplacement implements
+ ExceptionHandler<UnsatisfiedArgumentException, HttpResponse<? extends
Problem>> {
@Override
- public HttpResponse<? extends Problem> handle(HttpRequest request,
ConversionErrorException exception) {
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
UnsatisfiedArgumentException exception) {
return HttpProblemResponse.from(
Problem.fromHttpCode(HttpCode.BAD_REQUEST)
-
.detail(ExceptionUtils.getCause(exception).getMessage())
+ .detail(exception.getMessage())
);
}
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UnsatisfiedRouteHandlerReplacement.java
similarity index 68%
copy from
modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
copy to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UnsatisfiedRouteHandlerReplacement.java
index 094c630415..6744ec9be6 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UnsatisfiedRouteHandlerReplacement.java
@@ -15,33 +15,31 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.authentication.exception;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.server.exceptions.ExceptionHandler;
-import io.micronaut.security.authentication.AuthenticationException;
-import io.micronaut.security.authentication.AuthenticationExceptionHandler;
+import io.micronaut.http.server.exceptions.UnsatisfiedRouteHandler;
+import io.micronaut.web.router.exceptions.UnsatisfiedRouteException;
import jakarta.inject.Singleton;
import org.apache.ignite.internal.rest.api.Problem;
import org.apache.ignite.internal.rest.constants.HttpCode;
import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
/**
- * Replacement for {@link AuthenticationExceptionHandler}. Returns {@link
HttpProblemResponse}.
+ * Replacement for {@link UnsatisfiedRouteHandler} that returns {@link
Problem} instead of {@link HttpResponse}.
*/
@Singleton
-@Replaces(AuthenticationExceptionHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
-public class AuthenticationExceptionHandlerReplacement implements
- ExceptionHandler<AuthenticationException, HttpResponse<? extends
Problem>> {
-
+@Replaces(UnsatisfiedRouteHandler.class)
+@Requires(classes = {UnsatisfiedRouteException.class, ExceptionHandler.class})
+public class UnsatisfiedRouteHandlerReplacement implements
ExceptionHandler<UnsatisfiedRouteException, HttpResponse<? extends Problem>> {
@Override
- public HttpResponse<? extends Problem> handle(HttpRequest request,
AuthenticationException exception) {
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
UnsatisfiedRouteException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.UNAUTHORIZED)
+ Problem.fromHttpCode(HttpCode.BAD_REQUEST)
.detail(exception.getMessage())
);
}
diff --git
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UriSyntaxHandlerReplacement.java
similarity index 68%
rename from
modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
rename to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UriSyntaxHandlerReplacement.java
index 094c630415..2f9f034366 100644
---
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/authentication/exception/AuthenticationExceptionHandlerReplacement.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/replacement/UriSyntaxHandlerReplacement.java
@@ -15,33 +15,32 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.rest.authentication.exception;
+package org.apache.ignite.internal.rest.exception.handler.replacement;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.server.exceptions.ExceptionHandler;
-import io.micronaut.security.authentication.AuthenticationException;
-import io.micronaut.security.authentication.AuthenticationExceptionHandler;
+import io.micronaut.http.server.exceptions.URISyntaxHandler;
import jakarta.inject.Singleton;
+import java.net.URISyntaxException;
import org.apache.ignite.internal.rest.api.Problem;
import org.apache.ignite.internal.rest.constants.HttpCode;
import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
/**
- * Replacement for {@link AuthenticationExceptionHandler}. Returns {@link
HttpProblemResponse}.
+ * Replacement for {@link URISyntaxHandler} that returns {@link Problem}
instead of {@link HttpResponse}.
*/
@Singleton
-@Replaces(AuthenticationExceptionHandler.class)
-@Requires(classes = {Exception.class, ExceptionHandler.class})
-public class AuthenticationExceptionHandlerReplacement implements
- ExceptionHandler<AuthenticationException, HttpResponse<? extends
Problem>> {
-
+@Replaces(URISyntaxHandler.class)
+@Requires(classes = {URISyntaxException.class, ExceptionHandler.class})
+public class UriSyntaxHandlerReplacement implements
ExceptionHandler<URISyntaxException, HttpResponse<? extends Problem>> {
@Override
- public HttpResponse<? extends Problem> handle(HttpRequest request,
AuthenticationException exception) {
+ public HttpResponse<? extends Problem> handle(HttpRequest request,
URISyntaxException exception) {
return HttpProblemResponse.from(
- Problem.fromHttpCode(HttpCode.UNAUTHORIZED)
+ Problem.fromHttpCode(HttpCode.BAD_REQUEST)
+ .title("Malformed URI")
.detail(exception.getMessage())
);
}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/HttpProblemResponse.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/HttpProblemResponse.java
index 2bfe6ba67b..dfeb436d51 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/HttpProblemResponse.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/HttpProblemResponse.java
@@ -17,6 +17,8 @@
package org.apache.ignite.internal.rest.problem;
+import static
org.apache.ignite.internal.rest.problem.ProblemJsonMediaType.APPLICATION_JSON_PROBLEM_TYPE;
+
import io.micronaut.http.HttpResponseFactory;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MutableHttpResponse;
@@ -34,7 +36,10 @@ public final class HttpProblemResponse {
* Create {@link MutableHttpResponse} from {@link Problem}.
*/
public static MutableHttpResponse<Problem> from(Problem problem) {
- return
HttpResponseFactory.INSTANCE.status(HttpStatus.valueOf(problem.status())).body(problem);
+ return HttpResponseFactory.INSTANCE
+ .status(HttpStatus.valueOf(problem.status()))
+ .contentType(APPLICATION_JSON_PROBLEM_TYPE)
+ .body(problem);
}
/**
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/HttpProblemResponse.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/ProblemJsonMediaType.java
similarity index 50%
copy from
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/HttpProblemResponse.java
copy to
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/ProblemJsonMediaType.java
index 2bfe6ba67b..f1ec5ce5da 100644
---
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/HttpProblemResponse.java
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/ProblemJsonMediaType.java
@@ -17,30 +17,18 @@
package org.apache.ignite.internal.rest.problem;
-import io.micronaut.http.HttpResponseFactory;
-import io.micronaut.http.HttpStatus;
-import io.micronaut.http.MutableHttpResponse;
-import org.apache.ignite.internal.rest.api.Problem;
-import org.apache.ignite.internal.rest.api.Problem.ProblemBuilder;
+import io.micronaut.http.MediaType;
/**
- * Creates {@link MutableHttpResponse} from {@link Problem}.
+ * Media type for problem json.
*/
-public final class HttpProblemResponse {
- private HttpProblemResponse() {
- }
-
+public final class ProblemJsonMediaType extends MediaType {
/**
- * Create {@link MutableHttpResponse} from {@link Problem}.
+ * Media type for problem json.
*/
- public static MutableHttpResponse<Problem> from(Problem problem) {
- return
HttpResponseFactory.INSTANCE.status(HttpStatus.valueOf(problem.status())).body(problem);
- }
+ public static final ProblemJsonMediaType APPLICATION_JSON_PROBLEM_TYPE =
new ProblemJsonMediaType("application/json+problem");
- /**
- * Create {@link MutableHttpResponse} from {@link ProblemBuilder}.
- */
- public static MutableHttpResponse<? extends Problem> from(ProblemBuilder
problemBuilder) {
- return from(problemBuilder.build());
+ private ProblemJsonMediaType(String name) {
+ super(name);
}
}
diff --git
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/ProblemJsonMediaTypeCodec.java
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/ProblemJsonMediaTypeCodec.java
new file mode 100644
index 0000000000..791a9ed72a
--- /dev/null
+++
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/problem/ProblemJsonMediaTypeCodec.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.problem;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.micronaut.core.io.buffer.ByteBuffer;
+import io.micronaut.core.io.buffer.ByteBufferFactory;
+import io.micronaut.core.type.Argument;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.codec.CodecException;
+import io.micronaut.http.codec.MediaTypeCodec;
+import jakarta.inject.Singleton;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Problem json media type codec.
+ */
+@Singleton
+public class ProblemJsonMediaTypeCodec implements MediaTypeCodec {
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Override
+ public Collection<MediaType> getMediaTypes() {
+ return List.of(ProblemJsonMediaType.APPLICATION_JSON_PROBLEM_TYPE);
+ }
+
+ @Override
+ public <T> T decode(Argument<T> type, InputStream inputStream) throws
CodecException {
+ try {
+ return mapper.readValue(inputStream, type.getType());
+ } catch (Exception e) {
+ throw new CodecException("Failed to decode input stream", e);
+ }
+ }
+
+ @Override
+ public <T> void encode(T object, OutputStream outputStream) throws
CodecException {
+ try {
+ mapper.writeValue(outputStream, object);
+ } catch (Exception e) {
+ throw new CodecException("Failed to encode output stream", e);
+ }
+ }
+
+ @Override
+ public <T> byte[] encode(T object) throws CodecException {
+ try {
+ return mapper.writeValueAsBytes(object);
+ } catch (Exception e) {
+ throw new CodecException("Failed to encode output stream", e);
+ }
+ }
+
+ @Override
+ public <T, B> ByteBuffer<B> encode(T object, ByteBufferFactory<?, B>
allocator) throws CodecException {
+ try {
+ return allocator.wrap(mapper.writeValueAsBytes(object));
+ } catch (Exception e) {
+ throw new CodecException("Failed to encode output stream", e);
+ }
+ }
+}
diff --git
a/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/EchoMessage.java
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/EchoMessage.java
new file mode 100644
index 0000000000..0d4e2f7884
--- /dev/null
+++
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/EchoMessage.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.exception.handler;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Echo message.
+ */
+public class EchoMessage {
+ private final String text;
+
+ @JsonCreator
+ public EchoMessage(@JsonProperty("text") String text) {
+ this.text = text;
+ }
+
+ @JsonProperty("text")
+ public String msg() {
+ return text;
+ }
+}
diff --git
a/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/ErrorHandlingTest.java
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/ErrorHandlingTest.java
new file mode 100644
index 0000000000..fea123629c
--- /dev/null
+++
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/ErrorHandlingTest.java
@@ -0,0 +1,306 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.exception.handler;
+
+import static org.apache.ignite.internal.rest.constants.HttpCode.BAD_REQUEST;
+import static
org.apache.ignite.internal.rest.constants.HttpCode.METHOD_NOT_ALLOWED;
+import static org.apache.ignite.internal.rest.constants.HttpCode.NOT_FOUND;
+import static
org.apache.ignite.internal.rest.constants.HttpCode.UNSUPPORTED_MEDIA_TYPE;
+import static
org.apache.ignite.internal.rest.problem.ProblemJsonMediaType.APPLICATION_JSON_PROBLEM_TYPE;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.micronaut.context.annotation.Bean;
+import io.micronaut.context.annotation.Factory;
+import io.micronaut.context.annotation.Property;
+import io.micronaut.core.bind.exceptions.UnsatisfiedArgumentException;
+import io.micronaut.core.type.Argument;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.MutableHttpRequest;
+import io.micronaut.http.client.HttpClient;
+import io.micronaut.http.client.annotation.Client;
+import io.micronaut.http.client.exceptions.HttpClientResponseException;
+import io.micronaut.http.uri.UriBuilder;
+import io.micronaut.security.authentication.AuthenticationException;
+import io.micronaut.security.authentication.AuthorizationException;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import java.net.URISyntaxException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
+import org.apache.ignite.internal.lang.IgniteInternalException;
+import org.apache.ignite.internal.rest.api.InvalidParam;
+import org.apache.ignite.internal.rest.api.Problem;
+import org.apache.ignite.internal.rest.constants.MediaType;
+import org.apache.ignite.lang.IgniteException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Error handling tests.
+ */
+@MicronautTest
+@Property(name = "micronaut.security.enabled", value = "false")
+public class ErrorHandlingTest {
+ @Inject
+ @Client("/test")
+ HttpClient client;
+
+ private final AtomicReference<Throwable> throwable = new
AtomicReference<>(new RuntimeException());
+
+ private static Stream<Arguments> testExceptions() {
+ return Stream.of(
+ // couldn't find a case when exception is thrown
+ Arguments.of(new
UnsatisfiedArgumentException(Argument.DOUBLE)),
+ // thrown when request uri is invalid, but it's not possible
to create such request with HttpClient (it validates uri)
+ Arguments.of(new URISyntaxException("uri", "reason")),
+ Arguments.of(new
AuthenticationException("authentication-exception")),
+ Arguments.of(new AuthorizationException(null)),
+ Arguments.of(new IgniteException("ignite-exception")),
+ Arguments.of(new
IgniteInternalCheckedException("ignite-internal-exception")),
+ Arguments.of(new
IgniteInternalException("ignite-internal-exception")),
+ Arguments.of(new RuntimeException("runtime-exception")),
+ Arguments.of(new Exception("exception"))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("testExceptions")
+ public void testExceptions(Throwable throwable) {
+ this.throwable.set(throwable);
+
+ // Invoke endpoint with not allowed method
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange("/test/throw-exception")
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+ assertEquals(response.code(), problem.status());
+ assertNotNull(problem.title());
+ }
+
+ @Test
+ public void endpoint404() {
+ // Invoke non-existing endpoint
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange("/endpoint404")
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(NOT_FOUND.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(NOT_FOUND.code(), problem.status());
+ assertEquals("Not Found", problem.title());
+ assertEquals("Requested resource not found: /test/endpoint404",
problem.detail());
+ }
+
+ @Test
+ public void invalidDataTypePathVariable() {
+ // Invoke endpoint with wrong path variable data type
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange("/list/abc")
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(BAD_REQUEST.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(BAD_REQUEST.code(), problem.status());
+ assertEquals("Invalid parameter", problem.title());
+ assertEquals("Failed to convert argument [id] for value [abc] due to:
For input string: \"abc\"", problem.detail());
+ }
+
+ @Test
+ public void requiredQueryValueNotSpecified() {
+ // Invoke endpoint without required query value
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange("/list")
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(BAD_REQUEST.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(BAD_REQUEST.code(), problem.status());
+ assertEquals("Bad Request", problem.title());
+ assertEquals("Required QueryValue [greatThan] not specified",
problem.detail());
+ }
+
+ @Test
+ public void invalidTypeQueryValue() {
+ // Invoke endpoint with wrong data type of request argument
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange("/list?greatThan=abc")
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(BAD_REQUEST.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(BAD_REQUEST.code(), problem.status());
+ assertEquals("Invalid parameter", problem.title());
+ assertEquals("Failed to convert argument [greatThan] for value [abc]
due to: For input string: \"abc\"", problem.detail());
+ }
+
+ @Test
+ public void invalidTypeQueryValue1() {
+ // Invoke endpoint with wrong request argument values
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () ->
client.toBlocking().exchange("/list?greatThan=-1&lessThan=11")
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(BAD_REQUEST.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(BAD_REQUEST.code(), problem.status());
+ assertEquals("Bad Request", problem.title());
+ assertEquals("Validation failed", problem.detail());
+
+ assertEquals(2, problem.invalidParams().size());
+
+ assertThat(problem.invalidParams(), containsInAnyOrder(
+ new InvalidParam("list.greatThan", "greatThan: must be greater
than or equal to 0"),
+ new InvalidParam("list.lessThan", "lessThan: must be less than
or equal to 10")
+ ));
+ }
+
+ @Test
+ public void postWithInvalidMediaType() {
+ // Invoke endpoint with invalid media type
+ MutableHttpRequest<String> request =
HttpRequest.POST(UriBuilder.of("/echo").build(), EchoMessage.class)
+ .contentType(MediaType.TEXT_PLAIN)
+ .body("text='qwe'");
+
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange(request,
Argument.of(EchoMessage.class), Argument.of(Problem.class))
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(UNSUPPORTED_MEDIA_TYPE.code(),
response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(UNSUPPORTED_MEDIA_TYPE.code(), problem.status());
+ assertEquals("Unsupported Media Type", problem.title());
+ assertEquals("Unsupported media type: text", problem.detail());
+ }
+
+ @Test
+ public void postWithInvalidJson() {
+ // Invoke endpoint with invalid json
+ MutableHttpRequest<String> request =
HttpRequest.POST(UriBuilder.of("/echo").build(), EchoMessage.class)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body("{text='qwe'");
+
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange(request,
Argument.of(EchoMessage.class), Argument.of(Problem.class))
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(BAD_REQUEST.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(BAD_REQUEST.code(), problem.status());
+ assertEquals("Invalid JSON", problem.title());
+ assertThat(problem.detail(), containsString("Unexpected character"));
+ }
+
+ @Test
+ public void postWithMissingBody() {
+ // Invoke endpoint with invalid json
+ MutableHttpRequest<String> request =
HttpRequest.POST(UriBuilder.of("/echo").build(), EchoMessage.class)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body("");
+
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange(request,
Argument.of(EchoMessage.class), Argument.of(Problem.class))
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(BAD_REQUEST.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(BAD_REQUEST.code(), problem.status());
+ assertEquals("Bad Request", problem.title());
+ assertThat(problem.detail(), containsString("Required Body [dto] not
specified"));
+ }
+
+ @Test
+ public void methodNotAllowed() {
+ // Invoke endpoint with not allowed method
+ MutableHttpRequest<String> request =
HttpRequest.GET(UriBuilder.of("/echo").build());
+
+ HttpClientResponseException thrown = Assertions.assertThrows(
+ HttpClientResponseException.class,
+ () -> client.toBlocking().exchange(request,
Argument.of(EchoMessage.class), Argument.of(Problem.class))
+ );
+
+ HttpResponse<?> response = thrown.getResponse();
+ Problem problem = response.getBody(Problem.class).get();
+
+ assertEquals(METHOD_NOT_ALLOWED.code(), response.status().getCode());
+ assertEquals(APPLICATION_JSON_PROBLEM_TYPE.getType(),
response.getContentType().get().getType());
+
+ assertEquals(METHOD_NOT_ALLOWED.code(), problem.status());
+ assertEquals("Method Not Allowed", problem.title());
+ assertEquals("Method not allowed: GET", problem.detail());
+ }
+
+ @Bean
+ @Factory
+ public ThrowableProvider exceptionThrowingService() {
+ return throwable::get;
+ }
+}
diff --git
a/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/TestController.java
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/TestController.java
new file mode 100644
index 0000000000..3b979f6dfd
--- /dev/null
+++
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/TestController.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.exception.handler;
+
+import io.micronaut.http.annotation.Body;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.annotation.Post;
+import io.micronaut.http.annotation.QueryValue;
+import java.util.List;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+
+/**
+ * Test controller.
+ */
+@Controller("/test")
+public class TestController {
+ private final ThrowableProvider throwableProvider;
+
+ public TestController(ThrowableProvider throwableProvider) {
+ this.throwableProvider = throwableProvider;
+ }
+
+ @Get("/throw-exception")
+ public String throwException() throws Throwable {
+ throw throwableProvider.throwable();
+ }
+
+ @Get("/list")
+ public List<EchoMessage> list(@QueryValue @Min(0) int greatThan,
@QueryValue(defaultValue = "10") @Max(10) int lessThan) {
+ return List.of();
+ }
+
+ @Get(value = "/list/{id}", produces = "application/json")
+ public int get(@PathVariable int id) {
+ return id;
+ }
+
+ @Post(value = "/echo", consumes = "application/json")
+ public EchoMessage echo(@Body EchoMessage dto) {
+ return dto;
+ }
+
+ @Get("/sleep")
+ public void sleep(@QueryValue(defaultValue = "1000") long millis) throws
InterruptedException {
+ Thread.sleep(millis);
+ }
+}
diff --git
a/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/ThrowableProvider.java
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/ThrowableProvider.java
new file mode 100644
index 0000000000..64006b24e8
--- /dev/null
+++
b/modules/rest-api/src/test/java/org/apache/ignite/internal/rest/exception/handler/ThrowableProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest.exception.handler;
+
+/**
+ * Provides {@link Throwable} instance.
+ */
+public interface ThrowableProvider {
+ /**
+ * Returns {@link Throwable} instance.
+ *
+ * @return {@link Throwable} instance.
+ */
+ Throwable throwable();
+}
diff --git
a/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
index 40111a8874..c3ffc1482e 100644
---
a/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
+++
b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/cluster/ItClusterManagementControllerTest.java
@@ -90,8 +90,8 @@ public class ItClusterManagementControllerTest extends
RestTestBase {
HttpClientResponseException thrownBeforeInit =
assertThrows(HttpClientResponseException.class,
() -> client.toBlocking().retrieve("state",
ClusterState.class));
- // Then status is 404: there is no "state"
- assertThat(thrownBeforeInit.getStatus(),
is(equalTo(HttpStatus.NOT_FOUND)));
+ // Then status is 409: cluster not initialized
+ assertThat(thrownBeforeInit.getStatus(),
is(equalTo(HttpStatus.CONFLICT)));
assertThat(
getProblem(thrownBeforeInit).detail(),
is(equalTo("Cluster not initialized. Call
/management/v1/cluster/init in order to initialize cluster"))