From 106c42e4cc411228dd50fdef52958a8b779446c2 Mon Sep 17 00:00:00 2001 From: iliax Date: Tue, 23 Mar 2021 16:44:57 +0300 Subject: [PATCH] ISSUE-257: ErrorResponse format description (#294) * ISSUE-257: ErrorResponse format description & processing added --- .../kafka/ui/config/ReadOnlyModeFilter.java | 4 +- .../ui/exception/CustomBaseException.java | 3 +- .../exception/DuplicateEntityException.java | 6 +- .../kafka/ui/exception/ErrorCode.java | 45 ++++++ .../ui/exception/GlobalErrorAttributes.java | 31 ---- .../GlobalErrorWebExceptionHandler.java | 135 +++++++++++++++--- .../kafka/ui/exception/NotFoundException.java | 5 +- .../kafka/ui/exception/ReadOnlyException.java | 15 -- .../ui/exception/ReadOnlyModeException.java | 14 ++ .../RebalanceInProgressException.java | 5 +- .../UnprocessableEntityException.java | 5 +- .../ui/exception/ValidationException.java | 5 +- .../main/resources/swagger/kafka-ui-api.yaml | 33 +++++ 13 files changed, 222 insertions(+), 84 deletions(-) create mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java delete mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorAttributes.java delete mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyException.java create mode 100644 kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyModeException.java diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java index 032cb03799..608f10120d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java @@ -1,7 +1,7 @@ package com.provectus.kafka.ui.config; import com.provectus.kafka.ui.exception.NotFoundException; -import com.provectus.kafka.ui.exception.ReadOnlyException; +import com.provectus.kafka.ui.exception.ReadOnlyModeException; import com.provectus.kafka.ui.service.ClustersStorage; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; @@ -45,6 +45,6 @@ public class ReadOnlyModeFilter implements WebFilter { return chain.filter(exchange); } - return Mono.error(ReadOnlyException::new); + return Mono.error(ReadOnlyModeException::new); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/CustomBaseException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/CustomBaseException.java index 92c031c4b3..ab1e3cf205 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/CustomBaseException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/CustomBaseException.java @@ -1,6 +1,5 @@ package com.provectus.kafka.ui.exception; -import org.springframework.http.HttpStatus; public abstract class CustomBaseException extends RuntimeException { public CustomBaseException() { @@ -23,5 +22,5 @@ public abstract class CustomBaseException extends RuntimeException { super(message, cause, enableSuppression, writableStackTrace); } - public abstract HttpStatus getResponseStatusCode(); + public abstract ErrorCode getErrorCode(); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/DuplicateEntityException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/DuplicateEntityException.java index 25e861703e..3f9bb3f083 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/DuplicateEntityException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/DuplicateEntityException.java @@ -1,7 +1,5 @@ package com.provectus.kafka.ui.exception; -import org.springframework.http.HttpStatus; - public class DuplicateEntityException extends CustomBaseException { public DuplicateEntityException(String message) { @@ -9,7 +7,7 @@ public class DuplicateEntityException extends CustomBaseException { } @Override - public HttpStatus getResponseStatusCode() { - return HttpStatus.CONFLICT; + public ErrorCode getErrorCode() { + return ErrorCode.DUPLICATED_ENTITY; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java new file mode 100644 index 0000000000..7432c293cd --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java @@ -0,0 +1,45 @@ +package com.provectus.kafka.ui.exception; + +import java.util.HashSet; +import org.apache.logging.log4j.LogManager; +import org.springframework.http.HttpStatus; + + +public enum ErrorCode { + + UNEXPECTED(5000, HttpStatus.INTERNAL_SERVER_ERROR), + BINDING_FAIL(4001, HttpStatus.BAD_REQUEST), + VALIDATION_FAIL(4002, HttpStatus.BAD_REQUEST), + ENTITY_NOT_FOUND(4003, HttpStatus.NOT_FOUND), + READ_ONLY_MODE_ENABLE(4004, HttpStatus.METHOD_NOT_ALLOWED), + REBALANCE_IN_PROGRESS(4005, HttpStatus.CONFLICT), + DUPLICATED_ENTITY(4006, HttpStatus.CONFLICT), + UNPROCESSABLE_ENTITY(4007, HttpStatus.UNPROCESSABLE_ENTITY); + + static { + // codes uniqueness check + var codes = new HashSet(); + for (ErrorCode value : ErrorCode.values()) { + if (!codes.add(value.code())) { + LogManager.getLogger() + .warn("Multiple {} values refer to code {}", ErrorCode.class, value.code); + } + } + } + + private final int code; + private final HttpStatus httpStatus; + + ErrorCode(int code, HttpStatus httpStatus) { + this.code = code; + this.httpStatus = httpStatus; + } + + public int code() { + return code; + } + + public HttpStatus httpStatus() { + return httpStatus; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorAttributes.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorAttributes.java deleted file mode 100644 index 25f0352b46..0000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorAttributes.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.provectus.kafka.ui.exception; - -import java.util.Map; -import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClientResponseException; -import org.springframework.web.reactive.function.server.ServerRequest; - -@Component -public class GlobalErrorAttributes extends DefaultErrorAttributes { - - public static final String STATUS = "status"; - - @Override - public Map getErrorAttributes(ServerRequest request, boolean includeStackTrace) { - Map errorAttrs = super.getErrorAttributes(request, includeStackTrace); - includeCustomErrorAttributes(request, errorAttrs); - return errorAttrs; - } - - private void includeCustomErrorAttributes(ServerRequest request, Map errorAttrs) { - Throwable error = getError(request); - if (error instanceof WebClientResponseException) { - var webClientError = (WebClientResponseException) error; - errorAttrs.put(STATUS, webClientError.getStatusCode()); - } else if (error instanceof CustomBaseException) { - var customBaseError = (CustomBaseException) error; - errorAttrs.put(STATUS, customBaseError.getResponseStatusCode()); - } - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java index ce922dd9fe..3bded1837c 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorWebExceptionHandler.java @@ -1,34 +1,40 @@ package com.provectus.kafka.ui.exception; +import com.google.common.collect.Sets; +import com.provectus.kafka.ui.model.ErrorResponse; +import java.math.BigDecimal; +import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ResponseStatusException; import reactor.core.publisher.Mono; -/** - * The order of our global error handler is -2 to give it a higher priority than the default - * {@link org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler} - * which is registered at @Order(-1). - */ + @Component -@Order(-2) +@Order(Ordered.HIGHEST_PRECEDENCE) public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { - public GlobalErrorWebExceptionHandler(GlobalErrorAttributes errorAttributes, + public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ApplicationContext applicationContext, ServerCodecConfigurer codecConfigurer) { @@ -42,14 +48,107 @@ public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHan } private Mono renderErrorResponse(ServerRequest request) { - Map errorAttributes = getErrorAttributes(request, false); - HttpStatus statusCode = Optional.ofNullable(errorAttributes.get(GlobalErrorAttributes.STATUS)) - .map(code -> code instanceof Integer ? HttpStatus.valueOf((Integer) code) : - (HttpStatus) code) - .orElse(HttpStatus.BAD_REQUEST); - return ServerResponse - .status(statusCode) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorAttributes)); + Throwable throwable = getError(request); + + // validation and params binding errors + if (throwable instanceof WebExchangeBindException) { + return render((WebExchangeBindException) throwable, request); + } + + // requests mapping & access errors + if (throwable instanceof ResponseStatusException) { + return render((ResponseStatusException) throwable, request); + } + + // custom exceptions + if (throwable instanceof CustomBaseException) { + return render((CustomBaseException) throwable, request); + } + + return renderDefault(throwable, request); } + + private Mono renderDefault(Throwable throwable, ServerRequest request) { + var response = new ErrorResponse() + .code(ErrorCode.UNEXPECTED.code()) + .message(throwable.getMessage()) + .requestId(requestId(request)) + .timestamp(currentTimestamp()); + return ServerResponse + .status(ErrorCode.UNEXPECTED.httpStatus()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response); + } + + private Mono render(CustomBaseException baseException, ServerRequest request) { + ErrorCode errorCode = baseException.getErrorCode(); + var response = new ErrorResponse() + .code(errorCode.code()) + .message(baseException.getMessage()) + .requestId(requestId(request)) + .timestamp(currentTimestamp()); + return ServerResponse + .status(errorCode.httpStatus()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response); + } + + private Mono render(WebExchangeBindException exception, ServerRequest request) { + Map> fieldErrorsMap = exception.getFieldErrors().stream() + .collect(Collectors + .toMap(FieldError::getField, f -> Set.of(extractFieldErrorMsg(f)), Sets::union)); + + var fieldsErrors = fieldErrorsMap.entrySet().stream() + .map(e -> { + var err = new com.provectus.kafka.ui.model.FieldError(); + err.setFieldName(e.getKey()); + err.setRestrictions(List.copyOf(e.getValue())); + return err; + }).collect(Collectors.toList()); + + var message = fieldsErrors.isEmpty() + ? exception.getMessage() + : "Fields validation failure"; + + var response = new ErrorResponse() + .code(ErrorCode.BINDING_FAIL.code()) + .message(message) + .requestId(requestId(request)) + .timestamp(currentTimestamp()) + .fieldsErrors(fieldsErrors); + return ServerResponse + .status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response); + } + + private Mono render(ResponseStatusException exception, ServerRequest request) { + String msg = coalesce(exception.getReason(), exception.getMessage(), "Server error"); + var response = new ErrorResponse() + .code(ErrorCode.UNEXPECTED.code()) + .message(msg) + .requestId(requestId(request)) + .timestamp(currentTimestamp()); + return ServerResponse + .status(exception.getStatus()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response); + } + + private String requestId(ServerRequest request) { + return request.exchange().getRequest().getId(); + } + + private BigDecimal currentTimestamp() { + return BigDecimal.valueOf(System.currentTimeMillis()); + } + + private String extractFieldErrorMsg(FieldError fieldError) { + return coalesce(fieldError.getDefaultMessage(), fieldError.getCode(), "Invalid field value"); + } + + private T coalesce(T... items) { + return Stream.of(items).filter(Objects::nonNull).findFirst().orElse(null); + } + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/NotFoundException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/NotFoundException.java index 8ee8e9e558..773c398b30 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/NotFoundException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/NotFoundException.java @@ -1,6 +1,5 @@ package com.provectus.kafka.ui.exception; -import org.springframework.http.HttpStatus; public class NotFoundException extends CustomBaseException { @@ -9,7 +8,7 @@ public class NotFoundException extends CustomBaseException { } @Override - public HttpStatus getResponseStatusCode() { - return HttpStatus.NOT_FOUND; + public ErrorCode getErrorCode() { + return ErrorCode.ENTITY_NOT_FOUND; } } \ No newline at end of file diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyException.java deleted file mode 100644 index 0a0bd0266b..0000000000 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.provectus.kafka.ui.exception; - -import org.springframework.http.HttpStatus; - -public class ReadOnlyException extends CustomBaseException { - - public ReadOnlyException() { - super("This cluster is in read-only mode."); - } - - @Override - public HttpStatus getResponseStatusCode() { - return HttpStatus.METHOD_NOT_ALLOWED; - } -} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyModeException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyModeException.java new file mode 100644 index 0000000000..52b9da91b3 --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyModeException.java @@ -0,0 +1,14 @@ +package com.provectus.kafka.ui.exception; + + +public class ReadOnlyModeException extends CustomBaseException { + + public ReadOnlyModeException() { + super("This cluster is in read-only mode."); + } + + @Override + public ErrorCode getErrorCode() { + return ErrorCode.READ_ONLY_MODE_ENABLE; + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/RebalanceInProgressException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/RebalanceInProgressException.java index d48633b97b..2f6f91c6e6 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/RebalanceInProgressException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/RebalanceInProgressException.java @@ -1,6 +1,5 @@ package com.provectus.kafka.ui.exception; -import org.springframework.http.HttpStatus; public class RebalanceInProgressException extends CustomBaseException { @@ -9,7 +8,7 @@ public class RebalanceInProgressException extends CustomBaseException { } @Override - public HttpStatus getResponseStatusCode() { - return HttpStatus.CONFLICT; + public ErrorCode getErrorCode() { + return ErrorCode.REBALANCE_IN_PROGRESS; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/UnprocessableEntityException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/UnprocessableEntityException.java index 96e356adcc..8598735951 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/UnprocessableEntityException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/UnprocessableEntityException.java @@ -1,6 +1,5 @@ package com.provectus.kafka.ui.exception; -import org.springframework.http.HttpStatus; public class UnprocessableEntityException extends CustomBaseException { @@ -9,7 +8,7 @@ public class UnprocessableEntityException extends CustomBaseException { } @Override - public HttpStatus getResponseStatusCode() { - return HttpStatus.UNPROCESSABLE_ENTITY; + public ErrorCode getErrorCode() { + return ErrorCode.UNPROCESSABLE_ENTITY; } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java index e7bc61a5cb..7b964fbca5 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ValidationException.java @@ -1,6 +1,5 @@ package com.provectus.kafka.ui.exception; -import org.springframework.http.HttpStatus; public class ValidationException extends CustomBaseException { public ValidationException(String message) { @@ -8,7 +7,7 @@ public class ValidationException extends CustomBaseException { } @Override - public HttpStatus getResponseStatusCode() { - return HttpStatus.BAD_REQUEST; + public ErrorCode getErrorCode() { + return ErrorCode.VALIDATION_FAIL; } } diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml index 493290c0f2..e00efd1909 100644 --- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml +++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml @@ -1058,6 +1058,39 @@ paths: components: schemas: + ErrorResponse: + description: Error object that will be returned with 4XX and 5XX HTTP statuses + type: object + properties: + code: + type: integer + description: Internal error code (can be used for message formatting & localization on UI) + message: + type: string + description: Error message + timestamp: + type: number + description: Response unix timestamp in ms + requestId: + type: string + description: Unique server-defined request id for convenient debugging + fieldsErrors: + type: array + items: + $ref: '#/components/schemas/FieldError' + + FieldError: + type: object + properties: + fieldName: + type: string + description: Name of field that violated format + restrictions: + description: Field format violations description (ex. ["size must be between 0 and 20", "must be a well-formed email address"]) + type: array + items: + type: string + Cluster: type: object properties: