diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt index ca8b224b6e..09d307d2f4 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt @@ -36,7 +36,6 @@ import com.egm.stellio.shared.util.getAuthzContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext import com.egm.stellio.shared.util.parseRepresentations import com.egm.stellio.shared.util.replaceDefaultContextToAuthzContext -import com.egm.stellio.shared.util.toErrorResponse import com.egm.stellio.shared.web.BaseHandler import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt index b52097e2ad..e9dc6cb8c0 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt @@ -25,7 +25,6 @@ import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext import com.egm.stellio.shared.util.parsePaginationParameters import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders -import com.egm.stellio.shared.util.toErrorResponse import com.egm.stellio.shared.web.BaseHandler import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt index a6eb37a1f7..b4d1a37fd9 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt @@ -10,7 +10,6 @@ import com.egm.stellio.shared.util.decode import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders -import com.egm.stellio.shared.util.toErrorResponse import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt index 2581a02c10..1cf4ebb9c8 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt @@ -10,7 +10,6 @@ import com.egm.stellio.shared.util.decode import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders -import com.egm.stellio.shared.util.toErrorResponse import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt index 4c2ae9fe40..dc88acc99a 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt @@ -44,7 +44,6 @@ import com.egm.stellio.shared.util.missingPathErrorResponse import com.egm.stellio.shared.util.parseRepresentations import com.egm.stellio.shared.util.parseTimeParameter import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders -import com.egm.stellio.shared.util.toErrorResponse import com.egm.stellio.shared.util.toUri import com.egm.stellio.shared.web.BaseHandler import org.springframework.http.HttpHeaders diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt index 33d2d6e790..1fa5f2d3f7 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt @@ -36,7 +36,6 @@ import com.egm.stellio.shared.util.getContextFromLinkHeader import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext import com.egm.stellio.shared.util.parseRepresentations -import com.egm.stellio.shared.util.toErrorResponse import com.egm.stellio.shared.util.toListOfUri import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt index a885853579..b2d0842604 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt @@ -35,7 +35,6 @@ import com.egm.stellio.shared.util.getSubFromSecurityContext import com.egm.stellio.shared.util.invalidTemporalInstanceMessage import com.egm.stellio.shared.util.missingPathErrorResponse import com.egm.stellio.shared.util.parseRepresentations -import com.egm.stellio.shared.util.toErrorResponse import com.egm.stellio.shared.util.toUri import com.egm.stellio.shared.web.BaseHandler import org.springframework.http.HttpHeaders diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt index 40a37bcc84..0a7065922c 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt @@ -11,7 +11,6 @@ import com.egm.stellio.shared.util.JsonLdUtils.compactEntities import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext -import com.egm.stellio.shared.util.toErrorResponse import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders import org.springframework.http.MediaType diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt index 028a5c5b95..0845daf930 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt @@ -203,7 +203,7 @@ class EntityHandlerTests { { "type":"https://uri.etsi.org/ngsi-ld/errors/InternalError", "title":"There has been an error during the operation execution", - "detail":"InternalErrorException(message=Internal Server Exception)" + "detail":"Internal Server Exception" } """ ) diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/ApiExceptions.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/ApiExceptions.kt index 6a92693357..7ddf6e6490 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/ApiExceptions.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/ApiExceptions.kt @@ -2,24 +2,164 @@ package com.egm.stellio.shared.model import com.apicatalog.jsonld.JsonLdError import com.apicatalog.jsonld.JsonLdErrorCode +import com.egm.stellio.shared.util.JsonUtils.serializeObject +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import org.slf4j.LoggerFactory +import org.springframework.core.io.buffer.DefaultDataBufferFactory +import org.springframework.http.HttpHeaders.CONTENT_TYPE +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ProblemDetail +import org.springframework.http.ResponseEntity +import org.springframework.web.server.ServerWebExchange +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.net.URI + +const val DEFAULT_DETAIL = "If you have difficulty finding the issue" + + "you can check common issues here https://stellio.readthedocs.io/en/latest/TROUBLESHOOT.html" + + "If you think this is a bug of the broker" + + "you can create an issue on https://github.com/stellio-hub/stellio-context-broker" sealed class APIException( - override val message: String -) : Exception(message) - -data class InvalidRequestException(override val message: String) : APIException(message) -data class BadRequestDataException(override val message: String) : APIException(message) -data class AlreadyExistsException(override val message: String) : APIException(message) -data class OperationNotSupportedException(override val message: String) : APIException(message) -data class ResourceNotFoundException(override val message: String) : APIException(message) -data class InternalErrorException(override val message: String) : APIException(message) -data class TooComplexQueryException(override val message: String) : APIException(message) -data class TooManyResultsException(override val message: String) : APIException(message) -data class AccessDeniedException(override val message: String) : APIException(message) -data class NotImplementedException(override val message: String) : APIException(message) -data class LdContextNotAvailableException(override val message: String) : APIException(message) -data class NonexistentTenantException(override val message: String) : APIException(message) -data class NotAcceptableException(override val message: String) : APIException(message) + val type: URI, + @JsonIgnore + val status: HttpStatus, + open val title: String, + @JsonProperty("detail") + override val message: String = DEFAULT_DETAIL +) : Exception(message) { + private val logger = LoggerFactory.getLogger(javaClass) + + fun toErrorResponse(): ResponseEntity { + logger.info("Returning error ${this.type} (${this.message})") + return ResponseEntity.status(status) + .contentType(MediaType.APPLICATION_JSON) + .body( + ProblemDetail.forStatusAndDetail(status, this.message).also { + it.title = this.title + it.type = this.type + } + ) + } + + fun toServerWebExchange(exchange: ServerWebExchange): Mono { + logger.info("Returning server web exchange error ${this.type} (${this.message})") + exchange.response.statusCode = this.status + exchange.response.headers[CONTENT_TYPE] = MediaType.APPLICATION_JSON_VALUE + val body = serializeObject(this) + return exchange.response.writeWith( + Flux.just(DefaultDataBufferFactory().wrap(body.toByteArray())) + ) + } +} + +data class AlreadyExistsException(override val message: String) : APIException( + ErrorType.ALREADY_EXISTS.type, + HttpStatus.CONFLICT, + "The referred element already exists", + message +) + +data class InvalidRequestException(override val message: String) : APIException( // todo check + ErrorType.INVALID_REQUEST.type, + HttpStatus.BAD_REQUEST, + "The request associated to the operation is syntactically invalid or includes wrong content", + message +) + +data class BadRequestDataException(override val message: String) : APIException( + ErrorType.BAD_REQUEST_DATA.type, + HttpStatus.BAD_REQUEST, + "The request includes input data which does not meet the requirements of the operation", + message +) +data class OperationNotSupportedException(override val message: String) : APIException( + ErrorType.OPERATION_NOT_SUPPORTED.type, + HttpStatus.BAD_REQUEST, + "The operation is not supported", + message +) +data class ResourceNotFoundException(override val message: String) : APIException( + ErrorType.RESOURCE_NOT_FOUND.type, + HttpStatus.NOT_FOUND, + "The referred resource has not been found", + message +) +data class InternalErrorException(override val message: String) : APIException( + ErrorType.INTERNAL_ERROR.type, + HttpStatus.INTERNAL_SERVER_ERROR, + "There has been an error during the operation execution", + message +) + +data class TooManyResultsException(override val message: String) : APIException( + ErrorType.TOO_MANY_RESULTS.type, + HttpStatus.FORBIDDEN, + "The query associated to the operation is producing so many results " + + "that can exhaust client or server resources. " + + "It should be made more restrictive", + message +) +data class AccessDeniedException(override val message: String) : APIException( + ErrorType.ACCESS_DENIED.type, + HttpStatus.FORBIDDEN, + "The request tried to access an unauthorized resource", + message +) +data class NotImplementedException(override val message: String) : APIException( + ErrorType.NOT_IMPLEMENTED.type, + HttpStatus.NOT_IMPLEMENTED, + "The requested functionality is not yet implemented", + message +) +data class LdContextNotAvailableException(override val message: String) : APIException( + ErrorType.LD_CONTEXT_NOT_AVAILABLE.type, + HttpStatus.SERVICE_UNAVAILABLE, + "A remote JSON-LD @context referenced in a request cannot be retrieved by the NGSI-LD Broker and " + + "expansion or compaction cannot be performed", + message +) +data class NonexistentTenantException(override val message: String) : APIException( + ErrorType.NONEXISTENT_TENANT.type, + HttpStatus.NOT_FOUND, + "The addressed tenant does not exist", + message +) +data class TooComplexQueryException(override val message: String) : APIException( // todo check + ErrorType.TOO_COMPLEX_QUERY.type, + HttpStatus.INTERNAL_SERVER_ERROR, // todo check + "The query associated to the operation is too complex and cannot be resolved", + message +) +data class NotAcceptableException(override val message: String) : APIException( + ErrorType.NOT_ACCEPTABLE.type, + HttpStatus.NOT_ACCEPTABLE, + "The media type provided in Accept header is not supported", + message +) + +data class JsonParseApiException(override val message: String) : APIException( + ErrorType.BAD_REQUEST_DATA.type, + HttpStatus.BAD_REQUEST, + "The request includes invalid input data, an error occurred during JSON parsing", + message +) + +data class UnsupportedMediaTypeStatusApiException(override val message: String) : APIException( + ErrorType.UNSUPPORTED_MEDIA_TYPE.type, + HttpStatus.UNSUPPORTED_MEDIA_TYPE, + "The content type of the request is not supported", + message +) + +data class JsonLdErrorApiResponse(override val title: String, val detail: String) : APIException( + ErrorType.BAD_REQUEST_DATA.type, // todo check + HttpStatus.BAD_REQUEST, + title, + detail +) fun Throwable.toAPIException(specificMessage: String? = null): APIException = when (this) { @@ -32,3 +172,20 @@ fun Throwable.toAPIException(specificMessage: String? = null): APIException = else BadRequestDataException("Unexpected error while parsing payload (cause was: $this)") else -> BadRequestDataException(specificMessage ?: this.localizedMessage) } + +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")), + NOT_ACCEPTABLE(URI("https://uri.etsi.org/ngsi-ld/errors/NotAcceptable")), + NONEXISTENT_TENANT(URI("https://uri.etsi.org/ngsi-ld/errors/NonexistentTenant")) +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/ErrorResponse.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/ErrorResponse.kt deleted file mode 100644 index 3e7820cb76..0000000000 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/ErrorResponse.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.egm.stellio.shared.model - -import java.net.URI - -sealed class ErrorResponse( - val type: URI, - open val title: String, - open val detail: String -) - -data class InvalidRequestResponse(override val detail: String) : ErrorResponse( - ErrorType.INVALID_REQUEST.type, - "The request associated to the operation is syntactically invalid or includes wrong content", - detail -) - -data class BadRequestDataResponse(override val detail: String) : ErrorResponse( - ErrorType.BAD_REQUEST_DATA.type, - "The request includes input data which does not meet the requirements of the operation", - detail -) - -data class AlreadyExistsResponse(override val detail: String) : - ErrorResponse(ErrorType.ALREADY_EXISTS.type, "The referred element already exists", detail) - -data class OperationNotSupportedResponse(override val detail: String) : - ErrorResponse(ErrorType.OPERATION_NOT_SUPPORTED.type, "The operation is not supported", detail) - -data class ResourceNotFoundResponse(override val detail: String) : - ErrorResponse(ErrorType.RESOURCE_NOT_FOUND.type, "The referred resource has not been found", detail) - -data class InternalErrorResponse(override val detail: String) : - ErrorResponse(ErrorType.INTERNAL_ERROR.type, "There has been an error during the operation execution", detail) - -data class TooComplexQueryResponse(override val detail: String) : ErrorResponse( - ErrorType.TOO_COMPLEX_QUERY.type, - "The query associated to the operation is too complex and cannot be resolved", - detail -) - -data class TooManyResultsResponse(override val detail: String) : ErrorResponse( - ErrorType.TOO_MANY_RESULTS.type, - "The query associated to the operation is producing so many results " + - "that can exhaust client or server resources. " + - "It should be made more restrictive", - detail -) - -data class LdContextNotAvailableResponse(override val detail: String) : ErrorResponse( - ErrorType.LD_CONTEXT_NOT_AVAILABLE.type, - "A remote JSON-LD @context referenced in a request cannot be retrieved by the NGSI-LD Broker and " + - "expansion or compaction cannot be performed", - detail -) - -data class JsonLdErrorResponse(override val title: String, override val detail: String) : - ErrorResponse( - ErrorType.BAD_REQUEST_DATA.type, - title, - detail - ) - -data class JsonParseErrorResponse(override val detail: String) : ErrorResponse( - ErrorType.INVALID_REQUEST.type, - "The request includes invalid input data, an error occurred during JSON parsing", - detail -) - -data class AccessDeniedResponse(override val detail: String) : - ErrorResponse( - ErrorType.ACCESS_DENIED.type, - "The request tried to access an unauthorized resource", - detail - ) - -data class NotImplementedResponse(override val detail: String) : - ErrorResponse( - ErrorType.NOT_IMPLEMENTED.type, - "The requested functionality is not yet implemented", - detail - ) - -data class UnsupportedMediaTypeResponse(override val detail: String) : - ErrorResponse( - ErrorType.UNSUPPORTED_MEDIA_TYPE.type, - "The content type of the request is not supported", - detail - ) - -data class NotAcceptableResponse(override val detail: String) : - ErrorResponse( - ErrorType.NOT_ACCEPTABLE.type, - "The media type provided in Accept header is not supported", - detail - ) - -data class NonexistentTenantResponse(override val detail: String) : - ErrorResponse( - ErrorType.NONEXISTENT_TENANT.type, - "The addressed tenant does not exist", - 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")), - NOT_ACCEPTABLE(URI("https://uri.etsi.org/ngsi-ld/errors/NotAcceptable")), - NONEXISTENT_TENANT(URI("https://uri.etsi.org/ngsi-ld/errors/NonexistentTenant")) -} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt index 5c595e4c7f..dba87e30bd 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt @@ -1,27 +1,7 @@ package com.egm.stellio.shared.util -import com.egm.stellio.shared.model.APIException -import com.egm.stellio.shared.model.AccessDeniedException -import com.egm.stellio.shared.model.AccessDeniedResponse -import com.egm.stellio.shared.model.AlreadyExistsException -import com.egm.stellio.shared.model.AlreadyExistsResponse import com.egm.stellio.shared.model.BadRequestDataException -import com.egm.stellio.shared.model.BadRequestDataResponse -import com.egm.stellio.shared.model.ErrorResponse -import com.egm.stellio.shared.model.InternalErrorResponse -import com.egm.stellio.shared.model.InvalidRequestException -import com.egm.stellio.shared.model.InvalidRequestResponse -import com.egm.stellio.shared.model.LdContextNotAvailableException -import com.egm.stellio.shared.model.LdContextNotAvailableResponse -import com.egm.stellio.shared.model.NotImplementedException -import com.egm.stellio.shared.model.NotImplementedResponse -import com.egm.stellio.shared.model.OperationNotSupportedException -import com.egm.stellio.shared.model.OperationNotSupportedResponse import com.egm.stellio.shared.model.PaginationQuery -import com.egm.stellio.shared.model.ResourceNotFoundException -import com.egm.stellio.shared.model.ResourceNotFoundResponse -import com.egm.stellio.shared.model.TooManyResultsException -import com.egm.stellio.shared.model.TooManyResultsResponse import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY import com.egm.stellio.shared.util.JsonUtils.serializeObject import org.slf4j.LoggerFactory @@ -84,40 +64,6 @@ const val ENTITY_DOES_NOT_EXIST_MESSAGE = "Entity does not exist" private val logger = LoggerFactory.getLogger("com.egm.stellio.shared.util.ApiResponses") -/** - * this is globally duplicating what is in ExceptionHandler#transformErrorResponse() - * but main code there should move here when we no longer raise business exceptions - */ -fun APIException.toErrorResponse(): ResponseEntity<*> = - when (this) { - is AlreadyExistsException -> - generateErrorResponse(HttpStatus.CONFLICT, AlreadyExistsResponse(this.message)) - is ResourceNotFoundException -> - generateErrorResponse(HttpStatus.NOT_FOUND, ResourceNotFoundResponse(this.message)) - is InvalidRequestException -> - generateErrorResponse(HttpStatus.BAD_REQUEST, InvalidRequestResponse(this.message)) - is BadRequestDataException -> - generateErrorResponse(HttpStatus.BAD_REQUEST, BadRequestDataResponse(this.message)) - is OperationNotSupportedException -> - generateErrorResponse(HttpStatus.BAD_REQUEST, OperationNotSupportedResponse(this.message)) - is AccessDeniedException -> - generateErrorResponse(HttpStatus.FORBIDDEN, AccessDeniedResponse(this.message)) - is NotImplementedException -> - generateErrorResponse(HttpStatus.NOT_IMPLEMENTED, NotImplementedResponse(this.message)) - is LdContextNotAvailableException -> - generateErrorResponse(HttpStatus.SERVICE_UNAVAILABLE, LdContextNotAvailableResponse(this.message)) - is TooManyResultsException -> - generateErrorResponse(HttpStatus.FORBIDDEN, TooManyResultsResponse(this.message)) - else -> generateErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, InternalErrorResponse("$cause")) - } - -fun generateErrorResponse(status: HttpStatus, exception: ErrorResponse): ResponseEntity<*> { - logger.info("Returning error ${exception.type} (${exception.detail})") - return ResponseEntity.status(status) - .contentType(MediaType.APPLICATION_JSON) - .body(serializeObject(exception)) -} - fun missingPathErrorResponse(errorMessage: String): ResponseEntity<*> { logger.info("Bad Request: $errorMessage") return BadRequestDataException(errorMessage).toErrorResponse() diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt b/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt index da1904d887..7b808ed48d 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt @@ -1,30 +1,13 @@ package com.egm.stellio.shared.web import com.apicatalog.jsonld.JsonLdError -import com.egm.stellio.shared.model.AccessDeniedException -import com.egm.stellio.shared.model.AccessDeniedResponse -import com.egm.stellio.shared.model.AlreadyExistsException -import com.egm.stellio.shared.model.AlreadyExistsResponse -import com.egm.stellio.shared.model.BadRequestDataException -import com.egm.stellio.shared.model.BadRequestDataResponse -import com.egm.stellio.shared.model.ErrorResponse -import com.egm.stellio.shared.model.InternalErrorResponse -import com.egm.stellio.shared.model.InvalidRequestException -import com.egm.stellio.shared.model.InvalidRequestResponse -import com.egm.stellio.shared.model.JsonLdErrorResponse -import com.egm.stellio.shared.model.JsonParseErrorResponse -import com.egm.stellio.shared.model.LdContextNotAvailableException -import com.egm.stellio.shared.model.LdContextNotAvailableResponse -import com.egm.stellio.shared.model.NonexistentTenantException -import com.egm.stellio.shared.model.NonexistentTenantResponse -import com.egm.stellio.shared.model.NotAcceptableResponse -import com.egm.stellio.shared.model.NotImplementedException -import com.egm.stellio.shared.model.NotImplementedResponse -import com.egm.stellio.shared.model.ResourceNotFoundException -import com.egm.stellio.shared.model.ResourceNotFoundResponse -import com.egm.stellio.shared.model.UnsupportedMediaTypeResponse +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.InternalErrorException +import com.egm.stellio.shared.model.JsonLdErrorApiResponse +import com.egm.stellio.shared.model.JsonParseApiException +import com.egm.stellio.shared.model.NotAcceptableException +import com.egm.stellio.shared.model.UnsupportedMediaTypeStatusApiException import com.fasterxml.jackson.core.JsonParseException -import org.slf4j.LoggerFactory import org.springframework.core.codec.CodecException import org.springframework.http.HttpStatus import org.springframework.http.ProblemDetail @@ -38,75 +21,21 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException @RestControllerAdvice class ExceptionHandler { - private val logger = LoggerFactory.getLogger(javaClass) - @ExceptionHandler fun transformErrorResponse(throwable: Throwable): ResponseEntity = when (val cause = throwable.cause ?: throwable) { - is AlreadyExistsException -> generateErrorResponse( - HttpStatus.CONFLICT, - AlreadyExistsResponse(cause.message) - ) - is ResourceNotFoundException -> generateErrorResponse( - HttpStatus.NOT_FOUND, - ResourceNotFoundResponse(cause.message) - ) - is InvalidRequestException -> generateErrorResponse( - HttpStatus.BAD_REQUEST, - InvalidRequestResponse(cause.message) - ) - is BadRequestDataException -> generateErrorResponse( - HttpStatus.BAD_REQUEST, - BadRequestDataResponse(cause.message) - ) - is JsonLdError -> generateErrorResponse( - HttpStatus.BAD_REQUEST, - JsonLdErrorResponse(cause.code.toString(), cause.message.orEmpty()) - ) - is JsonParseException, is CodecException -> generateErrorResponse( - HttpStatus.BAD_REQUEST, - JsonParseErrorResponse(cause.message ?: "There has been a problem during JSON parsing") - ) - is AccessDeniedException -> generateErrorResponse( - HttpStatus.FORBIDDEN, - AccessDeniedResponse(cause.message) - ) - is NotImplementedException -> generateErrorResponse( - HttpStatus.NOT_IMPLEMENTED, - NotImplementedResponse(cause.message) - ) - is LdContextNotAvailableException -> generateErrorResponse( - HttpStatus.SERVICE_UNAVAILABLE, - LdContextNotAvailableResponse(cause.message) - ) - is UnsupportedMediaTypeStatusException -> generateErrorResponse( - HttpStatus.UNSUPPORTED_MEDIA_TYPE, - UnsupportedMediaTypeResponse(cause.message) - ) - is NotAcceptableStatusException -> generateErrorResponse( - HttpStatus.NOT_ACCEPTABLE, - NotAcceptableResponse(cause.message) - ) + is APIException -> cause.toErrorResponse() + is JsonLdError -> + JsonLdErrorApiResponse(cause.code.toString(), cause.message.orEmpty()).toErrorResponse() + is JsonParseException, is CodecException -> + JsonParseApiException(cause.message ?: "There has been a problem during JSON parsing").toErrorResponse() + is UnsupportedMediaTypeStatusException -> + UnsupportedMediaTypeStatusApiException(cause.message).toErrorResponse() + is NotAcceptableStatusException -> + NotAcceptableException(cause.message).toErrorResponse() is MethodNotAllowedException -> ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(cause.body) - is NonexistentTenantException -> generateErrorResponse( - HttpStatus.NOT_FOUND, - NonexistentTenantResponse(cause.message) - ) - else -> generateErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR, - InternalErrorResponse("$cause") - ) - } - private fun generateErrorResponse(status: HttpStatus, exception: ErrorResponse): ResponseEntity { - logger.info("Returning error ${exception.type} (${exception.detail})") - return ResponseEntity.status(status) - .body( - ProblemDetail.forStatusAndDetail(status, exception.detail).also { - it.title = exception.title - it.type = exception.type - } - ) - } + else -> InternalErrorException("$cause").toErrorResponse() + } } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/web/TenantWebFilter.kt b/shared/src/main/kotlin/com/egm/stellio/shared/web/TenantWebFilter.kt index 5ffc7db7df..6f6cf8843b 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/web/TenantWebFilter.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/web/TenantWebFilter.kt @@ -1,21 +1,15 @@ package com.egm.stellio.shared.web import com.egm.stellio.shared.config.ApplicationProperties -import com.egm.stellio.shared.model.NonexistentTenantResponse -import com.egm.stellio.shared.util.JsonUtils.serializeObject +import com.egm.stellio.shared.model.NonexistentTenantException import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory import org.springframework.core.Ordered import org.springframework.core.annotation.Order -import org.springframework.core.io.buffer.DefaultDataBufferFactory -import org.springframework.http.HttpHeaders.CONTENT_TYPE -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType import org.springframework.stereotype.Component import org.springframework.web.server.ServerWebExchange import org.springframework.web.server.WebFilter import org.springframework.web.server.WebFilterChain -import reactor.core.publisher.Flux import reactor.core.publisher.Mono const val NGSILD_TENANT_HEADER = "NGSILD-Tenant" @@ -44,12 +38,7 @@ class TenantWebFilter( val tenantName = tenantNameFromHeader ?: DEFAULT_TENANT_NAME if (!tenantsNames.contains(tenantName)) { logger.error("Unknown tenant requested: $tenantName") - exchange.response.setStatusCode(HttpStatus.NOT_FOUND) - exchange.response.headers[CONTENT_TYPE] = MediaType.APPLICATION_JSON_VALUE - val errorResponse = serializeObject(NonexistentTenantResponse("Tenant $tenantName does not exist")) - return exchange.response.writeWith( - Flux.just(DefaultDataBufferFactory().wrap(errorResponse.toByteArray())) - ) + return NonexistentTenantException("Tenant $tenantName does not exist").toServerWebExchange(exchange) } return chain diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt index 3411b0d962..c073a49458 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt @@ -26,7 +26,6 @@ import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext import com.egm.stellio.shared.util.parsePaginationParameters import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders -import com.egm.stellio.shared.util.toErrorResponse import com.egm.stellio.shared.web.BaseHandler import com.egm.stellio.subscription.model.Subscription import com.egm.stellio.subscription.model.serialize diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt index a5265142a4..7e2ec795ca 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt @@ -236,7 +236,7 @@ class SubscriptionHandlerTests { { "type":"https://uri.etsi.org/ngsi-ld/errors/InternalError", "title":"There has been an error during the operation execution", - "detail":"InternalErrorException(message=Internal Server Exception)" + "detail":"Internal Server Exception" } """ )