ISSUE-257: ErrorResponse format description (#294)
* ISSUE-257: ErrorResponse format description & processing added
This commit is contained in:
parent
992e8b0898
commit
106c42e4cc
13 changed files with 222 additions and 84 deletions
|
@ -1,7 +1,7 @@
|
||||||
package com.provectus.kafka.ui.config;
|
package com.provectus.kafka.ui.config;
|
||||||
|
|
||||||
import com.provectus.kafka.ui.exception.NotFoundException;
|
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 com.provectus.kafka.ui.service.ClustersStorage;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
@ -45,6 +45,6 @@ public class ReadOnlyModeFilter implements WebFilter {
|
||||||
return chain.filter(exchange);
|
return chain.filter(exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mono.error(ReadOnlyException::new);
|
return Mono.error(ReadOnlyModeException::new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.provectus.kafka.ui.exception;
|
package com.provectus.kafka.ui.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public abstract class CustomBaseException extends RuntimeException {
|
public abstract class CustomBaseException extends RuntimeException {
|
||||||
public CustomBaseException() {
|
public CustomBaseException() {
|
||||||
|
@ -23,5 +22,5 @@ public abstract class CustomBaseException extends RuntimeException {
|
||||||
super(message, cause, enableSuppression, writableStackTrace);
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract HttpStatus getResponseStatusCode();
|
public abstract ErrorCode getErrorCode();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package com.provectus.kafka.ui.exception;
|
package com.provectus.kafka.ui.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public class DuplicateEntityException extends CustomBaseException {
|
public class DuplicateEntityException extends CustomBaseException {
|
||||||
|
|
||||||
public DuplicateEntityException(String message) {
|
public DuplicateEntityException(String message) {
|
||||||
|
@ -9,7 +7,7 @@ public class DuplicateEntityException extends CustomBaseException {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getResponseStatusCode() {
|
public ErrorCode getErrorCode() {
|
||||||
return HttpStatus.CONFLICT;
|
return ErrorCode.DUPLICATED_ENTITY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +1,40 @@
|
||||||
package com.provectus.kafka.ui.exception;
|
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.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.ResourceProperties;
|
||||||
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
|
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
|
||||||
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||||
import org.springframework.stereotype.Component;
|
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.RequestPredicates;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
import reactor.core.publisher.Mono;
|
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
|
@Component
|
||||||
@Order(-2)
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
|
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
|
||||||
|
|
||||||
public GlobalErrorWebExceptionHandler(GlobalErrorAttributes errorAttributes,
|
public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes,
|
||||||
ResourceProperties resourceProperties,
|
ResourceProperties resourceProperties,
|
||||||
ApplicationContext applicationContext,
|
ApplicationContext applicationContext,
|
||||||
ServerCodecConfigurer codecConfigurer) {
|
ServerCodecConfigurer codecConfigurer) {
|
||||||
|
@ -42,14 +48,107 @@ public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHan
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
|
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
|
||||||
Map<String, Object> errorAttributes = getErrorAttributes(request, false);
|
Throwable throwable = getError(request);
|
||||||
HttpStatus statusCode = Optional.ofNullable(errorAttributes.get(GlobalErrorAttributes.STATUS))
|
|
||||||
.map(code -> code instanceof Integer ? HttpStatus.valueOf((Integer) code) :
|
// validation and params binding errors
|
||||||
(HttpStatus) code)
|
if (throwable instanceof WebExchangeBindException) {
|
||||||
.orElse(HttpStatus.BAD_REQUEST);
|
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
|
return ServerResponse
|
||||||
.status(statusCode)
|
.status(ErrorCode.UNEXPECTED.httpStatus())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.body(BodyInserters.fromValue(errorAttributes));
|
.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(errorCode.httpStatus())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.provectus.kafka.ui.exception;
|
package com.provectus.kafka.ui.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public class NotFoundException extends CustomBaseException {
|
public class NotFoundException extends CustomBaseException {
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ public class NotFoundException extends CustomBaseException {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getResponseStatusCode() {
|
public ErrorCode getErrorCode() {
|
||||||
return HttpStatus.NOT_FOUND;
|
return ErrorCode.ENTITY_NOT_FOUND;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.provectus.kafka.ui.exception;
|
package com.provectus.kafka.ui.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public class RebalanceInProgressException extends CustomBaseException {
|
public class RebalanceInProgressException extends CustomBaseException {
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ public class RebalanceInProgressException extends CustomBaseException {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getResponseStatusCode() {
|
public ErrorCode getErrorCode() {
|
||||||
return HttpStatus.CONFLICT;
|
return ErrorCode.REBALANCE_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.provectus.kafka.ui.exception;
|
package com.provectus.kafka.ui.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public class UnprocessableEntityException extends CustomBaseException {
|
public class UnprocessableEntityException extends CustomBaseException {
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ public class UnprocessableEntityException extends CustomBaseException {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getResponseStatusCode() {
|
public ErrorCode getErrorCode() {
|
||||||
return HttpStatus.UNPROCESSABLE_ENTITY;
|
return ErrorCode.UNPROCESSABLE_ENTITY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.provectus.kafka.ui.exception;
|
package com.provectus.kafka.ui.exception;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public class ValidationException extends CustomBaseException {
|
public class ValidationException extends CustomBaseException {
|
||||||
public ValidationException(String message) {
|
public ValidationException(String message) {
|
||||||
|
@ -8,7 +7,7 @@ public class ValidationException extends CustomBaseException {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpStatus getResponseStatusCode() {
|
public ErrorCode getErrorCode() {
|
||||||
return HttpStatus.BAD_REQUEST;
|
return ErrorCode.VALIDATION_FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1058,6 +1058,39 @@ paths:
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
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:
|
Cluster:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
Loading…
Add table
Reference in a new issue