Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor exception handling in REST API #11

Merged
merged 2 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 49 additions & 89 deletions src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ package fi.hsl.jore4.timetables.api

import fi.hsl.jore4.timetables.api.util.HasuraErrorExtensions
import fi.hsl.jore4.timetables.api.util.HasuraErrorResponse
import fi.hsl.jore4.timetables.api.util.InvalidTargetPriorityExtensions
import fi.hsl.jore4.timetables.api.util.MultipleTargetFramesFoundExtensions
import fi.hsl.jore4.timetables.api.util.PlainStatusExtensions
import fi.hsl.jore4.timetables.api.util.StagingVehicleScheduleFrameNotFoundExtensions
import fi.hsl.jore4.timetables.api.util.TargetPriorityParsingExtensions
import fi.hsl.jore4.timetables.api.util.TargetVehicleScheduleFrameNotFoundExtensions
import fi.hsl.jore4.timetables.enumerated.TimetablesPriority
import fi.hsl.jore4.timetables.service.CombineTimetablesService
import fi.hsl.jore4.timetables.service.InvalidTargetPriorityException
Expand Down Expand Up @@ -111,97 +117,51 @@ class TimetablesController(

@ExceptionHandler(RuntimeException::class)
fun handleRuntimeException(ex: RuntimeException): ResponseEntity<HasuraErrorResponse> {
LOGGER.error { "Exception during request:$ex" }
LOGGER.error(ex.stackTraceToString())

val httpStatus = HttpStatus.CONFLICT // Hasura only wants errors on 4xx range.
val hasuraErrorExtensions = HasuraErrorExtensions(httpStatus.value())
val hasuraErrorResponse = HasuraErrorResponse(ex.message, hasuraErrorExtensions)

return ResponseEntity(hasuraErrorResponse, httpStatus)
}

class InvalidTargetPriorityExtensions(
override val code: Int,
val targetPriority: TimetablesPriority
) : HasuraErrorExtensions(code)

@ExceptionHandler(InvalidTargetPriorityException::class)
fun handleInvalidTargetPriorityException(ex: InvalidTargetPriorityException): ResponseEntity<HasuraErrorResponse> {
val httpStatus = HttpStatus.BAD_REQUEST
val hasuraErrorExtensions = InvalidTargetPriorityExtensions(httpStatus.value(), ex.targetPriority)
val hasuraErrorResponse = HasuraErrorResponse(ex.message, hasuraErrorExtensions)

return ResponseEntity(hasuraErrorResponse, httpStatus)
}

class StagingVehicleScheduleFrameNotFoundExtensions(
override val code: Int,
val stagingVehicleScheduleFrameId: UUID
) : HasuraErrorExtensions(code)

@ExceptionHandler(StagingVehicleScheduleFrameNotFoundException::class)
fun handleStagingVehicleScheduleFrameNotFoundException(ex: StagingVehicleScheduleFrameNotFoundException): ResponseEntity<HasuraErrorResponse> {
val httpStatus = HttpStatus.NOT_FOUND
val hasuraErrorExtensions = StagingVehicleScheduleFrameNotFoundExtensions(
httpStatus.value(),
ex.stagingVehicleScheduleFrameId
)
val hasuraErrorResponse = HasuraErrorResponse(ex.message, hasuraErrorExtensions)

return ResponseEntity(hasuraErrorResponse, httpStatus)
}

class TargetVehicleScheduleFrameNotFoundExtensions(
override val code: Int,
val stagingVehicleScheduleFrameId: UUID
) : HasuraErrorExtensions(code)

@ExceptionHandler(TargetFrameNotFoundException::class)
fun handleTargetFrameNotFoundException(ex: TargetFrameNotFoundException): ResponseEntity<HasuraErrorResponse> {
val httpStatus = HttpStatus.NOT_FOUND
val hasuraErrorExtensions = TargetVehicleScheduleFrameNotFoundExtensions(
httpStatus.value(),
ex.stagingVehicleScheduleFrameId
)
val hasuraErrorResponse = HasuraErrorResponse(ex.message, hasuraErrorExtensions)

return ResponseEntity(hasuraErrorResponse, httpStatus)
}

class MultipleTargetFramesFoundExtensions(
override val code: Int,
val stagingVehicleScheduleFrameId: UUID,
val targetVehicleScheduleFrameIds: List<UUID>
) : HasuraErrorExtensions(code)

@ExceptionHandler(MultipleTargetFramesFoundException::class)
fun handleTargetFrameNotFoundException(ex: MultipleTargetFramesFoundException): ResponseEntity<HasuraErrorResponse> {
val httpStatus = HttpStatus.CONFLICT
val hasuraErrorExtensions = MultipleTargetFramesFoundExtensions(
httpStatus.value(),
ex.stagingVehicleScheduleFrameId,
ex.targetVehicleScheduleFrameIds
)
val hasuraErrorResponse = HasuraErrorResponse(ex.message, hasuraErrorExtensions)
val hasuraExtensions: HasuraErrorExtensions = when (ex) {
is InvalidTargetPriorityException -> {
InvalidTargetPriorityExtensions(HttpStatus.BAD_REQUEST, ex.targetPriority)
}

is StagingVehicleScheduleFrameNotFoundException -> {
StagingVehicleScheduleFrameNotFoundExtensions(HttpStatus.NOT_FOUND, ex.stagingVehicleScheduleFrameId)
}

is TargetFrameNotFoundException -> {
TargetVehicleScheduleFrameNotFoundExtensions(HttpStatus.NOT_FOUND, ex.stagingVehicleScheduleFrameId)
}

is MultipleTargetFramesFoundException -> {
MultipleTargetFramesFoundExtensions(
HttpStatus.CONFLICT,
ex.stagingVehicleScheduleFrameId,
ex.targetVehicleScheduleFrameIds
)
}

is TargetPriorityParsingException -> {
TargetPriorityParsingExtensions(HttpStatus.BAD_REQUEST, ex.targetPriority)
}

else -> {
LOGGER.error { "Exception during request:$ex" }
LOGGER.error(ex.stackTraceToString())

PlainStatusExtensions(HttpStatus.CONFLICT)
}
}

return ResponseEntity(hasuraErrorResponse, httpStatus)
}
val httpStatus: HttpStatus = hasuraExtensions.run {
if (code !in 400..499) {
LOGGER.warn { "Violating Hasura error response contract by returning code not like 4xx: $code" }
}

class TargetPriorityParsingExtensions(
override val code: Int,
val targetPriority: Int
) : HasuraErrorExtensions(code)

@ExceptionHandler(TargetPriorityParsingException::class)
fun handleIncompatibleTargetPriorityException(ex: TargetPriorityParsingException): ResponseEntity<HasuraErrorResponse> {
val httpStatus = HttpStatus.BAD_REQUEST
val hasuraErrorExtensions = TargetPriorityParsingExtensions(
httpStatus.value(),
ex.targetPriority
)
val hasuraErrorResponse = HasuraErrorResponse(ex.message, hasuraErrorExtensions)
HttpStatus.resolve(code) ?: run {
// This block should never be entered (and never will when using valid HTTP status codes).
LOGGER.warn { "Could not resolve HttpStatus from code $code" }
HttpStatus.BAD_REQUEST // default in case not resolved
}
}

return ResponseEntity(hasuraErrorResponse, httpStatus)
return ResponseEntity(HasuraErrorResponse(ex.message, hasuraExtensions), httpStatus)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package fi.hsl.jore4.timetables.api.util

import fi.hsl.jore4.timetables.enumerated.TimetablesPriority
import org.springframework.http.HttpStatus
import java.util.UUID

sealed interface HasuraErrorExtensions {

// code must be 4xx
val code: Int
}

data class PlainStatusExtensions(override val code: Int) : HasuraErrorExtensions {

constructor(httpStatus: HttpStatus) : this(httpStatus.value())
}

data class InvalidTargetPriorityExtensions(
override val code: Int,
val targetPriority: TimetablesPriority
) : HasuraErrorExtensions {

constructor(httpStatus: HttpStatus, targetPriority: TimetablesPriority) : this(httpStatus.value(), targetPriority)
}

data class StagingVehicleScheduleFrameNotFoundExtensions(
override val code: Int,
val stagingVehicleScheduleFrameId: UUID
) : HasuraErrorExtensions {

constructor(httpStatus: HttpStatus, stagingVehicleScheduleFrameId: UUID) : this(
httpStatus.value(),
stagingVehicleScheduleFrameId
)
}

data class TargetVehicleScheduleFrameNotFoundExtensions(
override val code: Int,
val stagingVehicleScheduleFrameId: UUID
) : HasuraErrorExtensions {

constructor(httpStatus: HttpStatus, stagingVehicleScheduleFrameId: UUID) : this(
httpStatus.value(),
stagingVehicleScheduleFrameId
)
}

data class MultipleTargetFramesFoundExtensions(
override val code: Int,
val stagingVehicleScheduleFrameId: UUID,
val targetVehicleScheduleFrameIds: List<UUID>
) : HasuraErrorExtensions {

constructor(
httpStatus: HttpStatus,
stagingVehicleScheduleFrameId: UUID,
targetVehicleScheduleFrameIds: List<UUID>
) : this(
httpStatus.value(),
stagingVehicleScheduleFrameId,
targetVehicleScheduleFrameIds
)
}

data class TargetPriorityParsingExtensions(
override val code: Int,
val targetPriority: Int
) : HasuraErrorExtensions {

constructor(httpStatus: HttpStatus, targetPriority: Int) : this(httpStatus.value(), targetPriority)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package fi.hsl.jore4.timetables.api.util

// See https://hasura.io/docs/latest/actions/action-handlers/#returning-an-error-response
open class HasuraErrorResponse(
class HasuraErrorResponse(
nullableMessage: String?,
val extensions: HasuraErrorExtensions
) {
val message = nullableMessage ?: "An error occurred."
}

open class HasuraErrorExtensions(
open val code: Int // Must be a 4xx code
)