Skip to content

Commit e4b2863

Browse files
committed
First try to implement a generic exception handler for handler functions
1 parent 2d7fe4a commit e4b2863

File tree

3 files changed

+57
-7
lines changed

3 files changed

+57
-7
lines changed

src/main/kotlin/com/github/mduesterhoeft/router/ApiException.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ open class ApiException(
66
val httpResponseStatus: Int,
77
val details: Map<String, Any> = emptyMap(),
88
cause: Throwable? = null
9-
) : RuntimeException("[$code] $message", cause) {
9+
) : RuntimeException(message, cause) {
1010

1111
override fun toString(): String {
1212
return "ApiException(message='$message', code='$code', httpResponseStatus=$httpResponseStatus, details=$details, cause=$cause)"

src/main/kotlin/com/github/mduesterhoeft/router/RequestHandler.kt

+24-6
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,30 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
1818

1919
open val objectMapper = jacksonObjectMapper()
2020

21+
abstract val router: Router
22+
2123
@Suppress("UNCHECKED_CAST")
2224
override fun handleRequest(input: APIGatewayProxyRequestEvent, context: Context): APIGatewayProxyResponseEvent? {
23-
log.info("handling request with method '${input.httpMethod}' and path '${input.path}' - Accept:${input.acceptHeader()} Content-Type:${input.contentType()} $input")
25+
log.debug("handling request with method '${input.httpMethod}' and path '${input.path}' - Accept:${input.acceptHeader()} Content-Type:${input.contentType()} $input")
2426
val routes = router.routes as List<RouterFunction<Any, Any>>
2527
val matchResults: List<RequestMatchResult> = routes.map { routerFunction: RouterFunction<Any, Any> ->
2628
val matchResult = routerFunction.requestPredicate.match(input)
27-
log.info("match result for route '$routerFunction' is '$matchResult'")
29+
log.debug("match result for route '$routerFunction' is '$matchResult'")
2830
if (matchResult.match) {
2931
val handler: HandlerFunction<Any, Any> = routerFunction.handler
3032
val requestBody = deserializeRequest(handler, input)
3133
val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern)
32-
val response = router.filter.then(handler as HandlerFunction<*, *>).invoke(request)
33-
return createResponse(input, response)
34+
return try {
35+
val response = router.filter.then(handler as HandlerFunction<*, *>).invoke(request)
36+
createResponse(input, response)
37+
} catch (e: RuntimeException) {
38+
when (e) {
39+
is ApiException -> createErrorResponse(input, e)
40+
.also { log.info("Caught api error while handling ${input.httpMethod} ${input.path} - $e") }
41+
else -> createInternalServerErrorResponse(input, e)
42+
.also { log.error("Caught exception handling ${input.httpMethod} ${input.path} - $e", e) }
43+
}
44+
}
3445
}
3546

3647
matchResult
@@ -88,8 +99,6 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
8899
)
89100
}
90101

91-
abstract val router: Router
92-
93102
open fun createErrorResponse(input: APIGatewayProxyRequestEvent, ex: ApiException): APIGatewayProxyResponseEvent =
94103
APIGatewayProxyResponseEvent()
95104
.withBody(objectMapper.writeValueAsString(mapOf(
@@ -100,6 +109,15 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
100109
.withStatusCode(ex.httpResponseStatus)
101110
.withHeaders(mapOf("Content-Type" to "application/json"))
102111

112+
open fun createInternalServerErrorResponse(input: APIGatewayProxyRequestEvent, ex: java.lang.RuntimeException): APIGatewayProxyResponseEvent =
113+
APIGatewayProxyResponseEvent()
114+
.withBody(objectMapper.writeValueAsString(mapOf(
115+
"message" to ex.message,
116+
"code" to "INTERNAL_SERVER_ERROR"
117+
)))
118+
.withStatusCode(500)
119+
.withHeaders(mapOf("Content-Type" to "application/json"))
120+
103121
open fun <T> createResponse(input: APIGatewayProxyRequestEvent, response: ResponseEntity<T>): APIGatewayProxyResponseEvent {
104122
val accept = MediaType.parse(input.acceptHeader())
105123
return when {

src/test/kotlin/com/github/mduesterhoeft/router/RequestHandlerTest.kt

+32
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,32 @@ class RequestHandlerTest {
159159
assert(handler.filterInvocations).isEqualTo(2)
160160
}
161161

162+
@Test
163+
fun `should handle api exception`() {
164+
165+
val response = testRequestHandler.handleRequest(
166+
APIGatewayProxyRequestEvent()
167+
.withPath("/some-api-exception")
168+
.withHttpMethod("GET")
169+
.withHeaders(mapOf("Accept" to "application/json")), mockk()
170+
)!!
171+
172+
assert(response.statusCode).isEqualTo(400)
173+
}
174+
175+
@Test
176+
fun `should handle internal server error`() {
177+
178+
val response = testRequestHandler.handleRequest(
179+
APIGatewayProxyRequestEvent()
180+
.withPath("/some-internal-server-error")
181+
.withHttpMethod("GET")
182+
.withHeaders(mapOf("Accept" to "application/json")), mockk()
183+
)!!
184+
185+
assert(response.statusCode).isEqualTo(500)
186+
}
187+
162188
class TestRequestHandlerWithFilter : RequestHandler() {
163189

164190
var filterInvocations = 0
@@ -187,6 +213,12 @@ class RequestHandlerTest {
187213
GET("/some") { _: Request<Unit> ->
188214
ResponseEntity.ok(TestResponse("Hello"))
189215
}
216+
GET<Unit, Unit>("/some-api-exception") {
217+
throw ApiException("boom", "BOOM", 400, mapOf("more" to "info"))
218+
}
219+
GET<Unit, Unit>("/some-internal-server-error") {
220+
throw IllegalArgumentException("boom")
221+
}
190222
GET("/some/{id}") { r: Request<Unit> ->
191223
ResponseEntity.ok(TestResponse("Hello ${r.getPathParameter("id")}"))
192224
}

0 commit comments

Comments
 (0)