diff --git a/apps/kafka-key-generator/nais/nais-dev.yaml b/apps/kafka-key-generator/nais/nais-dev.yaml index deef6eb7..c3e93043 100644 --- a/apps/kafka-key-generator/nais/nais-dev.yaml +++ b/apps/kafka-key-generator/nais/nais-dev.yaml @@ -52,3 +52,4 @@ spec: - application: paw-arbeidssoekerregisteret-utgang-pdl - application: paw-microfrontend-toggler - application: paw-arbeidssoekerregisteret-hendelselogg-backup + - application: paw-arbeidssoeker-bekreftelse-api diff --git a/lib/error-handling/build.gradle.kts b/lib/error-handling/build.gradle.kts index a925ada9..bf463ed4 100644 --- a/lib/error-handling/build.gradle.kts +++ b/lib/error-handling/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { compileOnly(ktorServer.core) + compileOnly(ktor.serializationJackson) compileOnly(orgApacheKafka.kafkaStreams) compileOnly(loggingLibs.logbackClassic) diff --git a/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt b/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt index a3f206d3..af71790f 100644 --- a/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt +++ b/lib/error-handling/src/main/kotlin/no/nav/paw/error/handler/HttpExceptionHandler.kt @@ -3,11 +3,13 @@ package no.nav.paw.error.handler import io.ktor.server.application.ApplicationCall import io.ktor.server.plugins.BadRequestException import io.ktor.server.plugins.ContentTransformationException +import io.ktor.server.request.ApplicationRequest import io.ktor.server.request.RequestAlreadyConsumedException import io.ktor.server.request.uri import io.ktor.server.response.respond import no.nav.paw.error.exception.ClientResponseException import no.nav.paw.error.exception.ServerResponseException +import no.nav.paw.error.model.ProblemDetails import no.nav.paw.error.model.build400Error import no.nav.paw.error.model.build500Error import no.nav.paw.error.model.buildError @@ -17,69 +19,62 @@ import org.slf4j.LoggerFactory private const val ERROR_TYPE_PREFIX = "PAW_" private val logger: Logger = LoggerFactory.getLogger("no.nav.paw.logger.error.http") -suspend fun ApplicationCall.handleException(throwable: T) { +suspend fun ApplicationCall.handleException(throwable: Throwable) { + val problemDetails = resolveProblemDetails(request, throwable) + logger.error(problemDetails.detail, throwable) + respond(problemDetails.status, problemDetails) +} + +fun resolveProblemDetails(request: ApplicationRequest, throwable: Throwable): ProblemDetails { when (throwable) { + is BadRequestException -> { + return build400Error( + "${ERROR_TYPE_PREFIX}KUNNE_IKKE_TOLKE_FORESPOERSEL", + "Kunne ikke tolke innhold i forespørsel", + request.uri + ) + } + is ContentTransformationException -> { - val error = build400Error( + return build400Error( "${ERROR_TYPE_PREFIX}KUNNE_IKKE_TOLKE_INNHOLD", "Kunne ikke tolke innhold i kall", - this.request.uri + request.uri ) - logger.debug(error.detail, throwable) - this.respond(error.status, error) } - is ClientResponseException -> { - val error = buildError( - "${ERROR_TYPE_PREFIX}${throwable.code}", - throwable.message, - throwable.status, - this.request.uri + is RequestAlreadyConsumedException -> { + return build500Error( + "${ERROR_TYPE_PREFIX}FORESPOERSEL_ALLEREDE_MOTTATT", + "Forespørsel er allerede mottatt. Dette er en kodefeil", + request.uri ) - logger.warn(error.detail, throwable) - this.respond(error.status, error) } is ServerResponseException -> { - val error = buildError( + return buildError( "${ERROR_TYPE_PREFIX}${throwable.code}", throwable.message, throwable.status, - this.request.uri + request.uri ) - logger.error(error.detail, throwable) - this.respond(error.status, error) - } - - is BadRequestException -> { - val error = - build400Error( - "${ERROR_TYPE_PREFIX}ULOVLIG_FORESPOERSEL", - "Kunne ikke tolke innhold i forespørsel", - this.request.uri - ) - logger.error(error.detail, throwable) - this.respond(error.status, error) } - is RequestAlreadyConsumedException -> { - val error = build500Error( - "${ERROR_TYPE_PREFIX}FORESPOERSEL_ALLEREDE_MOTTATT", - "Forespørsel er allerede mottatt. Dette er en kodefeil", - this.request.uri + is ClientResponseException -> { + return buildError( + "${ERROR_TYPE_PREFIX}${throwable.code}", + throwable.message, + throwable.status, + request.uri ) - logger.error(error.detail, throwable) - this.respond(error.status, error) } else -> { - val error = build500Error( + return build500Error( "${ERROR_TYPE_PREFIX}UKJENT_FEIL", "Forespørsel feilet med ukjent feil", - this.request.uri + request.uri ) - logger.error(error.detail, throwable) - this.respond(error.status, error) } } } \ No newline at end of file diff --git a/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt b/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt index 3a8181ca..8572a6cb 100644 --- a/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt +++ b/lib/error-handling/src/main/kotlin/no/nav/paw/error/model/ProblemDetails.kt @@ -1,6 +1,10 @@ package no.nav.paw.error.model +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize import io.ktor.http.HttpStatusCode +import no.nav.paw.error.serialize.HttpStatusCodeDeserializer +import no.nav.paw.error.serialize.HttpStatusCodeSerializer /** * Object som inneholder detaljer om en oppstått feilsituasjon, basert på RFC 7807. @@ -9,13 +13,13 @@ import io.ktor.http.HttpStatusCode data class ProblemDetails( val type: String, val title: String, - val status: HttpStatusCode, + @JsonSerialize(using = HttpStatusCodeSerializer::class) @JsonDeserialize(using = HttpStatusCodeDeserializer::class) val status: HttpStatusCode, val detail: String, val instance: String ) { constructor( title: String, - status: HttpStatusCode, + @JsonSerialize(using = HttpStatusCodeSerializer::class) @JsonDeserialize(using = HttpStatusCodeDeserializer::class) status: HttpStatusCode, detail: String, instance: String ) : this("about:blank", title, status, detail, instance) diff --git a/lib/error-handling/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeDeserializer.kt b/lib/error-handling/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeDeserializer.kt new file mode 100644 index 00000000..e175cf96 --- /dev/null +++ b/lib/error-handling/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeDeserializer.kt @@ -0,0 +1,12 @@ +package no.nav.paw.error.serialize + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import io.ktor.http.HttpStatusCode + +class HttpStatusCodeDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): HttpStatusCode { + return HttpStatusCode.fromValue(parser.numberValue.toInt()) + } +} \ No newline at end of file diff --git a/lib/error-handling/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeSerializer.kt b/lib/error-handling/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeSerializer.kt new file mode 100644 index 00000000..df0554bd --- /dev/null +++ b/lib/error-handling/src/main/kotlin/no/nav/paw/error/serialize/HttpStatusCodeSerializer.kt @@ -0,0 +1,12 @@ +package no.nav.paw.error.serialize + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import io.ktor.http.HttpStatusCode + +class HttpStatusCodeSerializer : JsonSerializer() { + override fun serialize(value: HttpStatusCode, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeNumber(value.value) + } +} \ No newline at end of file diff --git a/lib/error-handling/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt b/lib/error-handling/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt new file mode 100644 index 00000000..406711fb --- /dev/null +++ b/lib/error-handling/src/test/kotlin/no/nav/paw/error/handler/HttpExceptionHandlerTest.kt @@ -0,0 +1,55 @@ +package no.nav.paw.error.handler + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.jackson.jackson +import io.ktor.server.application.ApplicationCall +import io.ktor.server.plugins.BadRequestException +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.routing.IgnoreTrailingSlash +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import no.nav.paw.error.model.ProblemDetails +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation +import io.ktor.server.application.install as serverInstall +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +class HttpExceptionHandlerTest : FreeSpec({ + "Skal håndtere exceptions og returnere ProblemDetails response" { + testApplication { + application { + serverInstall(IgnoreTrailingSlash) + serverInstall(StatusPages) { + exception { call: ApplicationCall, cause: Throwable -> + call.handleException(cause) + } + } + serverInstall(ServerContentNegotiation) { + jackson {} + } + routing { + get("/api/400") { + throw BadRequestException("It's bad") + } + } + } + + val client = createClient { + install(ClientContentNegotiation) { + jackson {} + } + } + + val response400 = client.get("/api/400") + val responseBody404 = response400.body() + response400.status shouldBe HttpStatusCode.BadRequest + responseBody404.status shouldBe HttpStatusCode.BadRequest + responseBody404.type shouldBe "PAW_KUNNE_IKKE_TOLKE_FORESPOERSEL" + responseBody404.title shouldBe HttpStatusCode.BadRequest.description + } + } +}) \ No newline at end of file