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;
|
||||
|
||||
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,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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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);
|
||||
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<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(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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue