Explorar el Código

ISSUE-257: ErrorResponse format description (#294)

* ISSUE-257: ErrorResponse format description & processing added
iliax hace 4 años
padre
commit
106c42e4cc

+ 2 - 2
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);
   }
 }

+ 1 - 2
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();
 }

+ 2 - 4
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;
   }
 }

+ 45 - 0
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<Integer>();
+    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;
+  }
+}

+ 0 - 31
kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/GlobalErrorAttributes.java

@@ -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<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
-    Map<String, Object> errorAttrs = super.getErrorAttributes(request, includeStackTrace);
-    includeCustomErrorAttributes(request, errorAttrs);
-    return errorAttrs;
-  }
-
-  private void includeCustomErrorAttributes(ServerRequest request, Map<String, Object> 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());
-    }
-  }
-}

+ 115 - 16
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 <code>@Order(-1)</code>.
- */
+
 @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<ServerResponse> renderErrorResponse(ServerRequest request) {
-    Map<String, Object> 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);
+    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<ServerResponse> 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<ServerResponse> 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(statusCode)
+        .status(errorCode.httpStatus())
         .contentType(MediaType.APPLICATION_JSON)
-        .body(BodyInserters.fromValue(errorAttributes));
+        .bodyValue(response);
   }
+
+  private Mono<ServerResponse> render(WebExchangeBindException exception, ServerRequest request) {
+    Map<String, Set<String>> 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<ServerResponse> 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> T coalesce(T... items) {
+    return Stream.of(items).filter(Objects::nonNull).findFirst().orElse(null);
+  }
+
 }

+ 2 - 3
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;
   }
 }

+ 0 - 15
kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ReadOnlyException.java

@@ -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;
-  }
-}

+ 14 - 0
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;
+  }
+}

+ 2 - 3
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;
   }
 }

+ 2 - 3
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;
   }
 }

+ 2 - 3
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;
   }
 }

+ 33 - 0
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: