Skip to content

Commit

Permalink
Refactor exception handling for REST API.
Browse files Browse the repository at this point in the history
Make `HasuraErrorExtensions` class hierarchy a sealed class hierarchy.
  • Loading branch information
jarkkoka committed Oct 17, 2023
1 parent 762cd45 commit c77bf8b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 94 deletions.
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,38 @@
package fi.hsl.jore4.timetables.api.util

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

sealed class HasuraErrorExtensions(httpStatus: HttpStatus) {
// code must be 4xx
val code: Int = httpStatus.value()
}

class PlainStatusExtensions(httpStatus: HttpStatus) : HasuraErrorExtensions(httpStatus)

class InvalidTargetPriorityExtensions(
httpStatus: HttpStatus,
val targetPriority: TimetablesPriority
) : HasuraErrorExtensions(httpStatus)

class StagingVehicleScheduleFrameNotFoundExtensions(
httpStatus: HttpStatus,
val stagingVehicleScheduleFrameId: UUID
) : HasuraErrorExtensions(httpStatus)

class TargetVehicleScheduleFrameNotFoundExtensions(
httpStatus: HttpStatus,
val stagingVehicleScheduleFrameId: UUID
) : HasuraErrorExtensions(httpStatus)

class MultipleTargetFramesFoundExtensions(
httpStatus: HttpStatus,
val stagingVehicleScheduleFrameId: UUID,
val targetVehicleScheduleFrameIds: List<UUID>
) : HasuraErrorExtensions(httpStatus)

class TargetPriorityParsingExtensions(
httpStatus: HttpStatus,
val targetPriority: Int
) : HasuraErrorExtensions(httpStatus)
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
)

0 comments on commit c77bf8b

Please sign in to comment.