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"))