From 7c96fcf316128fae43b3d9d92afb583150f09064 Mon Sep 17 00:00:00 2001 From: "lukasz.gryzbon" Date: Mon, 28 Feb 2022 08:45:25 +0000 Subject: [PATCH] fix: Add ResponseStatusException handling --- README.md | 1 - .../config/ControllerExceptionHandler.kt | 18 ++++++++- .../exception/ErrorResponseExceptionShould.kt | 8 ++-- .../exception/ExceptionIntegrationShould.kt | 38 ++++++++++++++++--- .../exception/TestResponseStatusException.kt | 7 ++++ .../testapp/controller/TestController.kt | 18 +++++++-- ...tatedResponseStatusException.approved.json | 10 +++++ ...seStatusExceptionWithMessage.approved.json | 10 +++++ ...tatusExceptionWithoutMessage.approved.json | 10 +++++ 9 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/api/exception/TestResponseStatusException.kt create mode 100644 server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507AnnotatedResponseStatusException.approved.json create mode 100644 server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithMessage.approved.json create mode 100644 server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithoutMessage.approved.json diff --git a/README.md b/README.md index 3dac9fe..c3e3a7c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ ![Maven Central](https://img.shields.io/maven-central/v/io.github.lsd-consulting/spring-http-exception-handling-library-api) ## TODO -* add tests for ResponseStatusException * add "failedAt" to the list of attributes * add test for CustomResponseEntityExceptionHandler#handleBindException diff --git a/server/src/main/kotlin/com/lsdconsulting/exceptionhandling/server/config/ControllerExceptionHandler.kt b/server/src/main/kotlin/com/lsdconsulting/exceptionhandling/server/config/ControllerExceptionHandler.kt index b4141c9..e7a23e0 100644 --- a/server/src/main/kotlin/com/lsdconsulting/exceptionhandling/server/config/ControllerExceptionHandler.kt +++ b/server/src/main/kotlin/com/lsdconsulting/exceptionhandling/server/config/ControllerExceptionHandler.kt @@ -10,12 +10,15 @@ import org.springframework.core.Ordered import org.springframework.core.annotation.AnnotatedElementUtils import org.springframework.core.annotation.Order import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.context.request.WebRequest +import org.springframework.web.server.ResponseStatusException import javax.validation.ConstraintViolation import javax.validation.ConstraintViolationException @@ -31,6 +34,7 @@ class ControllerExceptionHandler( protected fun handleUncaughtException(ex: Exception, request: WebRequest): ResponseEntity<*> { return when (ex) { is ConstraintViolationException -> handle(ex, request) + is ResponseStatusException -> handle(ex, request) is ErrorResponseException -> handle(ex, request) else -> ResponseEntity(unknownErrorHandler.handle(ex, request), getResponseStatusFromAnnotation(ex)) } @@ -41,6 +45,16 @@ class ControllerExceptionHandler( return ResponseEntity(errorResponse, ex.httpStatus) } + private fun handle(ex: ResponseStatusException, request: WebRequest): ResponseEntity<*> { + val errorResponse = ErrorResponse( + errorCode = ex.status.name, +// messages = if (ex.message != null) listOf(ex.message!!) else listOf(), + messages = listOf(ex.message!!), + attributes = attributePopulator.populateAttributes(ex, request) + ) + return ResponseEntity(errorResponse, ex.status) + } + private fun handle(ex: ConstraintViolationException, request: WebRequest): ResponseEntity<*> { val bodyDataErrors = ex.constraintViolations .map { constraintViolation: ConstraintViolation<*> -> convert(constraintViolation) } @@ -51,13 +65,13 @@ class ControllerExceptionHandler( dataErrors = bodyDataErrors, attributes = attributePopulator.populateAttributes(ex, request) ) - return ResponseEntity(errorResponse, HttpStatus.BAD_REQUEST) + return ResponseEntity(errorResponse, BAD_REQUEST) } private fun getResponseStatusFromAnnotation(ex: Exception): HttpStatus { // check if exception was annotated with @ResponseStatus val responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.javaClass, ResponseStatus::class.java) - return responseStatus?.code ?: HttpStatus.INTERNAL_SERVER_ERROR + return responseStatus?.code ?: INTERNAL_SERVER_ERROR } private fun convert(constraintViolation: ConstraintViolation<*>): DataError { diff --git a/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/exception/ErrorResponseExceptionShould.kt b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/exception/ErrorResponseExceptionShould.kt index fee1e9a..416fa2d 100644 --- a/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/exception/ErrorResponseExceptionShould.kt +++ b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/exception/ErrorResponseExceptionShould.kt @@ -6,7 +6,7 @@ import org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` import org.junit.jupiter.api.Test -import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatus.OK internal class ErrorResponseExceptionShould { @@ -15,21 +15,21 @@ internal class ErrorResponseExceptionShould { @Test fun preserveExceptionMessageThroughErrorDetailResponseConstructor() { val result: ErrorResponseException = - object : ErrorResponseException(ErrorResponse(messages = listOf(message)), HttpStatus.OK) {} + object : ErrorResponseException(ErrorResponse(messages = listOf(message)), OK) {} assertThat(result.message, `is`(message)) } @Test fun preserveMessageThroughErrorDetailResponseConstructor() { val result: ErrorResponseException = - object : ErrorResponseException(ErrorResponse(messages = listOf(message)), HttpStatus.OK) {} + object : ErrorResponseException(ErrorResponse(messages = listOf(message)), OK) {} assertThat(result.errorResponse.messages[0], `is`(message)) } @Test fun handleEmptyMessageThroughErrorDetailResponseConstructor() { val result: ErrorResponseException = - object : ErrorResponseException(ErrorResponse(), HttpStatus.OK) {} + object : ErrorResponseException(ErrorResponse(), OK) {} assertThat(result.message, `is`("Error message unavailable")) } } diff --git a/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.kt b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.kt index 7784365..04b5bb9 100644 --- a/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.kt +++ b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.kt @@ -19,8 +19,7 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.cloud.openfeign.EnableFeignClients import org.springframework.context.annotation.Import -import org.springframework.http.HttpStatus -import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import org.springframework.http.HttpStatus.* import org.springframework.http.ResponseEntity import org.springframework.test.context.TestPropertySource import java.io.IOException @@ -82,7 +81,7 @@ class ExceptionIntegrationShould( fun return404TestObjectNotFoundException(approver: Approver) { val responseEntity = testRestTemplate.getForEntity("/objects/objectNotFoundException", ErrorResponse::class.java) - assertThat(responseEntity.statusCode, `is`(HttpStatus.NOT_FOUND)) + assertThat(responseEntity.statusCode, `is`(NOT_FOUND)) approver.assertApproved(asString(responseEntity.body!!)) } @@ -91,7 +90,7 @@ class ExceptionIntegrationShould( fun return405ForHttpRequestMethodNotSupportedException(approver: Approver) { val responseEntity = testRestTemplate.postForEntity("/objects/1", TestRequest(), ErrorResponse::class.java) - assertThat(responseEntity.statusCode, `is`(HttpStatus.METHOD_NOT_ALLOWED)) + assertThat(responseEntity.statusCode, `is`(METHOD_NOT_ALLOWED)) approver.assertApproved(asString(responseEntity)) } @@ -100,7 +99,7 @@ class ExceptionIntegrationShould( fun return409AnnotatedStatusOnException(approver: Approver) { val responseEntity = testRestTemplate.getForEntity("/objects/generateAnnotatedException", ErrorResponse::class.java) - assertThat(responseEntity.statusCode, `is`(HttpStatus.CONFLICT)) + assertThat(responseEntity.statusCode, `is`(CONFLICT)) approver.assertApproved(asString(responseEntity.body!!)) } @@ -109,7 +108,34 @@ class ExceptionIntegrationShould( fun return409AnnotatedStatusOnExceptionWithMessage(approver: Approver) { val responseEntity = testRestTemplate.getForEntity("/objects/generateAnnotatedExceptionWithMessage", ErrorResponse::class.java) - assertThat(responseEntity.statusCode, `is`(HttpStatus.CONFLICT)) + assertThat(responseEntity.statusCode, `is`(CONFLICT)) + approver.assertApproved(asString(responseEntity.body!!)) + } + + @Test + @Throws(IOException::class) + fun return507ResponseStatusExceptionWithMessage(approver: Approver) { + val responseEntity = testRestTemplate.getForEntity("/objects/generateResponseStatusException", ErrorResponse::class.java) + + assertThat(responseEntity.statusCode, `is`(INSUFFICIENT_STORAGE)) + approver.assertApproved(asString(responseEntity.body!!)) + } + + @Test + @Throws(IOException::class) + fun return507ResponseStatusExceptionWithoutMessage(approver: Approver) { + val responseEntity = testRestTemplate.getForEntity("/objects/generateResponseStatusExceptionNoMessage", ErrorResponse::class.java) + + assertThat(responseEntity.statusCode, `is`(INSUFFICIENT_STORAGE)) + approver.assertApproved(asString(responseEntity.body!!)) + } + + @Test + @Throws(IOException::class) + fun return507AnnotatedResponseStatusException(approver: Approver) { + val responseEntity = testRestTemplate.getForEntity("/objects/generateAnnotatedResponseStatusException", ErrorResponse::class.java) + + assertThat(responseEntity.statusCode, `is`(INSUFFICIENT_STORAGE)) approver.assertApproved(asString(responseEntity.body!!)) } diff --git a/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/api/exception/TestResponseStatusException.kt b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/api/exception/TestResponseStatusException.kt new file mode 100644 index 0000000..86de17f --- /dev/null +++ b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/api/exception/TestResponseStatusException.kt @@ -0,0 +1,7 @@ +package com.lsdconsulting.exceptionhandling.server.testapp.api.exception + +import org.springframework.http.HttpStatus.INSUFFICIENT_STORAGE +import org.springframework.web.server.ResponseStatusException +import java.io.IOException + +class TestResponseStatusException : ResponseStatusException(INSUFFICIENT_STORAGE, "Insufficient storage", IOException()) diff --git a/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/controller/TestController.kt b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/controller/TestController.kt index af2ca35..5fc2195 100644 --- a/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/controller/TestController.kt +++ b/server/src/test/kotlin/com/lsdconsulting/exceptionhandling/server/testapp/controller/TestController.kt @@ -2,10 +2,7 @@ package com.lsdconsulting.exceptionhandling.server.testapp.controller import com.lsdconsulting.exceptionhandling.api.ErrorResponse import com.lsdconsulting.exceptionhandling.server.exception.ErrorResponseException -import com.lsdconsulting.exceptionhandling.server.testapp.api.exception.TestAnnotatedException -import com.lsdconsulting.exceptionhandling.server.testapp.api.exception.TestException -import com.lsdconsulting.exceptionhandling.server.testapp.api.exception.TestObjectNotFoundException -import com.lsdconsulting.exceptionhandling.server.testapp.api.exception.TestParameterException +import com.lsdconsulting.exceptionhandling.server.testapp.api.exception.* import com.lsdconsulting.exceptionhandling.server.testapp.api.request.IsoDateTimeRequest import com.lsdconsulting.exceptionhandling.server.testapp.api.request.TestRequest import com.lsdconsulting.exceptionhandling.server.testapp.api.response.TestResponse @@ -13,6 +10,7 @@ import org.springframework.http.HttpStatus.* import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* +import org.springframework.web.server.ResponseStatusException import java.time.ZonedDateTime import javax.validation.Valid import javax.validation.constraints.Max @@ -91,6 +89,18 @@ class TestController { messages = listOf("some message")), PRECONDITION_FAILED ) {} + @GetMapping("/generateResponseStatusException") + fun getWithResponseStatusException(): Unit = + throw ResponseStatusException(INSUFFICIENT_STORAGE, "Insufficient storage") + + @GetMapping("/generateResponseStatusExceptionNoMessage") + fun getWithResponseStatusExceptionNoMessage(): Unit = + throw ResponseStatusException(INSUFFICIENT_STORAGE) + + @GetMapping("/generateAnnotatedResponseStatusException") + fun getWithAnnotatedResponseStatusException(): Unit = + throw TestResponseStatusException() + @GetMapping("/malformedResponse") fun getMalformedResponse(@RequestParam responseCode: Int): ResponseEntity = ResponseEntity.status(responseCode).body("blah") diff --git a/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507AnnotatedResponseStatusException.approved.json b/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507AnnotatedResponseStatusException.approved.json new file mode 100644 index 0000000..8f2f04a --- /dev/null +++ b/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507AnnotatedResponseStatusException.approved.json @@ -0,0 +1,10 @@ +{ + "errorCode" : "INSUFFICIENT_STORAGE", + "messages" : [ "507 INSUFFICIENT_STORAGE \"Insufficient storage\"; nested exception is java.io.IOException" ], + "dataErrors" : [ ], + "attributes" : { + "traceId" : "40e1488ed0001adc", + "exception" : "IOException", + "startTime" : "2020-11-27T11:17:40.095818Z" + } +} \ No newline at end of file diff --git a/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithMessage.approved.json b/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithMessage.approved.json new file mode 100644 index 0000000..3293395 --- /dev/null +++ b/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithMessage.approved.json @@ -0,0 +1,10 @@ +{ + "errorCode" : "INSUFFICIENT_STORAGE", + "messages" : [ "507 INSUFFICIENT_STORAGE \"Insufficient storage\"" ], + "dataErrors" : [ ], + "attributes" : { + "traceId" : "40e1488ed0001adc", + "exception" : "ResponseStatusException", + "startTime" : "2020-11-27T11:17:40.095818Z" + } +} \ No newline at end of file diff --git a/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithoutMessage.approved.json b/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithoutMessage.approved.json new file mode 100644 index 0000000..20f9f68 --- /dev/null +++ b/server/src/test/resources/data/com/lsdconsulting/exceptionhandling/server/integration/exception/ExceptionIntegrationShould.return507ResponseStatusExceptionWithoutMessage.approved.json @@ -0,0 +1,10 @@ +{ + "errorCode" : "INSUFFICIENT_STORAGE", + "messages" : [ "507 INSUFFICIENT_STORAGE" ], + "dataErrors" : [ ], + "attributes" : { + "traceId" : "40e1488ed0001adc", + "exception" : "ResponseStatusException", + "startTime" : "2020-11-27T11:17:40.095818Z" + } +} \ No newline at end of file