Skip to content

Commit

Permalink
fix(common): update custom error handling
Browse files Browse the repository at this point in the history
- ProblemDetails support in SB is not totally finished (spring-projects/spring-boot#33885, spring-projects/spring-boot#19525), stick to semi-manual implementation
  • Loading branch information
bobeal committed Jan 31, 2023
1 parent 2122d1c commit 2ad370c
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ spring.flyway.url = jdbc:postgresql://postgres/stellio_search

spring.kafka.bootstrap-servers = kafka:9092

server.error.include-stacktrace = never

spring.devtools.add-properties = false

application.authentication.enabled = true
2 changes: 2 additions & 0 deletions search-service/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ spring.kafka.consumer.auto-offset-reset = earliest
# By default, new matching topics are checked every 5 minutes but it can be configured by overriding the following prop
# spring.kafka.consumer.properties.metadata.max.age.ms = 1000

server.error.include-stacktrace = always

# cf https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html#_specifying_the_authorization_server
spring.security.oauth2.resourceserver.jwt.issuer-uri = https://sso.eglobalmark.com/auth/realms/stellio
# not required, but it avoids tying startup to authorization server's availability
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.egm.stellio.shared.model

import java.net.URI

sealed class ErrorResponse(
val type: String,
val type: URI,
open val title: String,
open val detail: String
)
Expand Down Expand Up @@ -67,26 +69,36 @@ data class JsonParseErrorResponse(override val detail: String) : ErrorResponse(

data class AccessDeniedResponse(override val detail: String) :
ErrorResponse(
"https://uri.etsi.org/ngsi-ld/errors/AccessDenied",
ErrorType.ACCESS_DENIED.type,
"The request tried to access an unauthorized resource",
detail
)

data class NotImplementedResponse(override val detail: String) :
ErrorResponse(
"https://uri.etsi.org/ngsi-ld/errors/NotImplemented",
ErrorType.NOT_IMPLEMENTED.type,
"The requested functionality is not yet implemented",
detail
)

enum class ErrorType(val type: String) {
INVALID_REQUEST("https://uri.etsi.org/ngsi-ld/errors/InvalidRequest"),
BAD_REQUEST_DATA("https://uri.etsi.org/ngsi-ld/errors/BadRequestData"),
ALREADY_EXISTS("https://uri.etsi.org/ngsi-ld/errors/AlreadyExists"),
OPERATION_NOT_SUPPORTED("https://uri.etsi.org/ngsi-ld/errors/OperationNotSupported"),
RESOURCE_NOT_FOUND("https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound"),
INTERNAL_ERROR("https://uri.etsi.org/ngsi-ld/errors/InternalError"),
TOO_COMPLEX_QUERY("https://uri.etsi.org/ngsi-ld/errors/TooComplexQuery"),
TOO_MANY_RESULTS("https://uri.etsi.org/ngsi-ld/errors/TooManyResults"),
LD_CONTEXT_NOT_AVAILABLE("https://uri.etsi.org/ngsi-ld/errors/LdContextNotAvailable")
data class UnsupportedMediaTypeResponse(override val detail: String) :
ErrorResponse(
ErrorType.UNSUPPORTED_MEDIA_TYPE.type,
"The content type of the request is not supported",
detail
)

enum class ErrorType(val type: URI) {
INVALID_REQUEST(URI("https://uri.etsi.org/ngsi-ld/errors/InvalidRequest")),
BAD_REQUEST_DATA(URI("https://uri.etsi.org/ngsi-ld/errors/BadRequestData")),
ALREADY_EXISTS(URI("https://uri.etsi.org/ngsi-ld/errors/AlreadyExists")),
OPERATION_NOT_SUPPORTED(URI("https://uri.etsi.org/ngsi-ld/errors/OperationNotSupported")),
RESOURCE_NOT_FOUND(URI("https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound")),
INTERNAL_ERROR(URI("https://uri.etsi.org/ngsi-ld/errors/InternalError")),
TOO_COMPLEX_QUERY(URI("https://uri.etsi.org/ngsi-ld/errors/TooComplexQuery")),
TOO_MANY_RESULTS(URI("https://uri.etsi.org/ngsi-ld/errors/TooManyResults")),
LD_CONTEXT_NOT_AVAILABLE(URI("https://uri.etsi.org/ngsi-ld/errors/LdContextNotAvailable")),
ACCESS_DENIED(URI("https://uri.etsi.org/ngsi-ld/errors/AccessDenied")),
NOT_IMPLEMENTED(URI("https://uri.etsi.org/ngsi-ld/errors/NotImplemented")),
UNSUPPORTED_MEDIA_TYPE(URI("https://uri.etsi.org/ngsi-ld/errors/UnsupportedMediaType"))
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package com.egm.stellio.shared.web

import com.egm.stellio.shared.model.*
import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.fasterxml.jackson.core.JsonParseException
import com.github.jsonldjava.core.JsonLdError
import org.slf4j.LoggerFactory
import org.springframework.core.codec.CodecException
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ProblemDetail
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.server.UnsupportedMediaTypeStatusException

@RestControllerAdvice
class ExceptionHandler {

private val logger = LoggerFactory.getLogger(javaClass)

@ExceptionHandler
fun transformErrorResponse(throwable: Throwable): ResponseEntity<String> =
fun transformErrorResponse(throwable: Throwable): ResponseEntity<ProblemDetail> =
when (val cause = throwable.cause ?: throwable) {
is AlreadyExistsException -> generateErrorResponse(
HttpStatus.CONFLICT,
Expand Down Expand Up @@ -56,16 +56,24 @@ class ExceptionHandler {
HttpStatus.SERVICE_UNAVAILABLE,
LdContextNotAvailableResponse(cause.message)
)
is UnsupportedMediaTypeStatusException -> generateErrorResponse(
HttpStatus.UNSUPPORTED_MEDIA_TYPE,
UnsupportedMediaTypeResponse(cause.message)
)
else -> generateErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
InternalErrorResponse("$cause")
)
}

private fun generateErrorResponse(status: HttpStatus, exception: ErrorResponse): ResponseEntity<String> {
private fun generateErrorResponse(status: HttpStatus, exception: ErrorResponse): ResponseEntity<ProblemDetail> {
logger.info("Returning error ${exception.type} (${exception.detail})")
return ResponseEntity.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body(serializeObject(exception))
.body(
ProblemDetail.forStatusAndDetail(status, exception.detail).also {
it.title = exception.title
it.type = exception.type
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ spring.flyway.url = jdbc:postgresql://postgres/stellio_subscription

spring.kafka.bootstrap-servers = kafka:9092

server.error.include-stacktrace = never

spring.devtools.add-properties = false

application.authentication.enabled = true
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ spring.kafka.consumer.auto-offset-reset = earliest
# By default, new matching topics are checked every 5 minutes but it can be configured by overriding the following prop
# spring.kafka.consumer.properties.metadata.max.age.ms = 1000

server.error.include-stacktrace = always

# cf https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html#_specifying_the_authorization_server
spring.security.oauth2.resourceserver.jwt.issuer-uri = https://sso.eglobalmark.com/auth/realms/stellio
# not required, but it avoids tying startup to authorization server's availability
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
import org.springframework.context.annotation.Import
import org.springframework.core.io.ClassPathResource
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.reactive.server.WebTestClient
Expand Down Expand Up @@ -233,6 +234,20 @@ class SubscriptionHandlerTests {
)
}

@Test
fun `create subscription should return a 415 if the content type is not correct`() {
val jsonLdFile = ClassPathResource("/ngsild/subscription.json")

webClient.post()
.uri("/ngsi-ld/v1/subscriptions")
.contentType(MediaType.APPLICATION_PDF)
.bodyValue(jsonLdFile)
.exchange()
.expectStatus().isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE)

coVerify { subscriptionService.validateNewSubscription(any()) wasNot Called }
}

@Test
fun `create subscription should return a 400 if validation of the subscription fails`() {
val jsonLdFile = ClassPathResource("/ngsild/subscription_with_conflicting_timeInterval_watchedAttributes.json")
Expand Down

0 comments on commit 2ad370c

Please sign in to comment.