improved REST-API error handling for broken request body JSON
This commit is contained in:
parent
e305c3c935
commit
77ef126a7e
@ -1,5 +1,6 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.openapitools.jackson.nullable.JsonNullableModule;
|
||||
@ -16,6 +17,7 @@ public class JsonObjectMapperConfiguration {
|
||||
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||
return new Jackson2ObjectMapperBuilder()
|
||||
.modules(new JsonNullableModule(), new JavaTimeModule())
|
||||
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS)
|
||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package net.hostsharing.hsadminng.errors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
class CustomErrorResponse {
|
||||
|
||||
static ResponseEntity<CustomErrorResponse> errorResponse(
|
||||
final WebRequest request,
|
||||
final HttpStatus httpStatus,
|
||||
final String message) {
|
||||
return new ResponseEntity<>(
|
||||
new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus);
|
||||
}
|
||||
|
||||
static String firstMessageLine(final Throwable exception) {
|
||||
if (exception.getMessage() != null) {
|
||||
return firstLine(exception.getMessage());
|
||||
}
|
||||
return "ERROR: [500] " + exception.getClass().getName();
|
||||
}
|
||||
|
||||
static String firstLine(final String message) {
|
||||
return message.split("\\r|\\n|\\r\\n", 0)[0];
|
||||
}
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
|
||||
private final LocalDateTime timestamp;
|
||||
|
||||
private final String path;
|
||||
|
||||
private final int status;
|
||||
|
||||
private final String error;
|
||||
|
||||
private final String message;
|
||||
|
||||
CustomErrorResponse(final String path, final HttpStatus status, final String message) {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
this.path = path;
|
||||
this.status = status.value();
|
||||
this.error = status.getReasonPhrase();
|
||||
this.message = message;
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
package net.hostsharing.hsadminng.errors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Getter;
|
||||
import org.iban4j.Iban4jException;
|
||||
import org.springframework.core.NestedExceptionUtils;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
@ -17,11 +16,12 @@ import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
import javax.persistence.EntityNotFoundException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
|
||||
|
||||
@ControllerAdvice
|
||||
public class RestResponseEntityExceptionHandler
|
||||
extends ResponseEntityExceptionHandler {
|
||||
@ -73,6 +73,16 @@ public class RestResponseEntityExceptionHandler
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked,rawtypes")
|
||||
protected ResponseEntity handleExceptionInternal(
|
||||
Exception exc, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||
|
||||
final var response = super.handleExceptionInternal(exc, body, headers, status, request);
|
||||
return errorResponse(request, response.getStatusCode(),
|
||||
Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc)));
|
||||
}
|
||||
|
||||
//@ExceptionHandler({ MethodArgumentNotValidException.class })
|
||||
@SuppressWarnings("unchecked,rawtypes")
|
||||
protected ResponseEntity handleMethodArgumentNotValid(
|
||||
MethodArgumentNotValidException exc,
|
||||
HttpHeaders headers,
|
||||
@ -126,45 +136,4 @@ public class RestResponseEntityExceptionHandler
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static ResponseEntity<CustomErrorResponse> errorResponse(
|
||||
final WebRequest request,
|
||||
final HttpStatus httpStatus,
|
||||
final String message) {
|
||||
return new ResponseEntity<>(
|
||||
new CustomErrorResponse(request.getContextPath(), httpStatus, message), httpStatus);
|
||||
}
|
||||
|
||||
private String firstMessageLine(final Throwable exception) {
|
||||
if (exception.getMessage() != null) {
|
||||
return firstLine(exception.getMessage());
|
||||
}
|
||||
return "ERROR: [500] " + exception.getClass().getName();
|
||||
}
|
||||
|
||||
private String firstLine(final String message) {
|
||||
return message.split("\\r|\\n|\\r\\n", 0)[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
class CustomErrorResponse {
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
|
||||
private final LocalDateTime timestamp;
|
||||
|
||||
private final String path;
|
||||
|
||||
private final int status;
|
||||
|
||||
private final String error;
|
||||
|
||||
private final String message;
|
||||
|
||||
public CustomErrorResponse(final String path, final HttpStatus status, final String message) {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
this.path = path;
|
||||
this.status = status.value();
|
||||
this.error = status.getReasonPhrase();
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ class HsOfficeBankAccountControllerAcceptanceTest {
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/hs/office/bankaccounts")
|
||||
.then().assertThat()
|
||||
.then().log().all().assertThat()
|
||||
.statusCode(201)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("uuid", isUuidValid())
|
||||
@ -346,7 +346,10 @@ class HsOfficeBankAccountControllerAcceptanceTest {
|
||||
jpaAttempt.transacted(() -> {
|
||||
context.define("superuser-alex@hostsharing.net", null);
|
||||
tempBankAccountUuids.addAll(
|
||||
bankAccountRepo.findByOptionalHolderLike("some temp acc").stream().map(HsOfficeBankAccountEntity::getUuid).toList()
|
||||
bankAccountRepo.findByOptionalHolderLike("some temp acc")
|
||||
.stream()
|
||||
.map(HsOfficeBankAccountEntity::getUuid)
|
||||
.toList()
|
||||
);
|
||||
});
|
||||
tempBankAccountUuids.forEach(uuid -> {
|
||||
|
@ -247,6 +247,26 @@ class TestCustomerControllerAcceptanceTest {
|
||||
context.define("superuser-fran@hostsharing.net");
|
||||
assertThat(testCustomerRepository.findCustomerByOptionalPrefixLike("uuu")).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidRequestBodyJson_raisesClientError() {
|
||||
|
||||
RestAssured // @formatter:off
|
||||
.given()
|
||||
.header("current-user", "superuser-alex@hostsharing.net")
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{]") // deliberately invalid JSON
|
||||
.port(port)
|
||||
.when()
|
||||
.post("http://localhost/api/test/customers")
|
||||
.then().assertThat()
|
||||
.statusCode(400)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("message", containsString("JSON parse error: Unexpected close marker ']': expected '}'"))
|
||||
.body("message", containsString("line: 1, column: 1"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private UUID toCleanup(final UUID tempPartnerUuid) {
|
||||
@ -262,7 +282,9 @@ class TestCustomerControllerAcceptanceTest {
|
||||
System.out.println("DELETING temporary partner: " + uuid);
|
||||
final var entity = testCustomerRepository.findByUuid(uuid);
|
||||
final var count = testCustomerRepository.deleteByUuid(uuid);
|
||||
System.out.println("DELETED temporary partner: " + uuid + (count > 0 ? " successful" : " failed") + " (" + entity.map(TestCustomerEntity::getPrefix).orElse("???") + ")");
|
||||
System.out.println(
|
||||
"DELETED temporary partner: " + uuid + (count > 0 ? " successful" : " failed") + " (" + entity.map(
|
||||
TestCustomerEntity::getPrefix).orElse("???") + ")");
|
||||
}).assertSuccessful();
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user