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 <gagarkin....@gmail.com>
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"))

Reply via email to