Skip to content

Commit 24c448a

Browse files
Add more standard error handling to request router (#4)
1 parent fc7a0bb commit 24c448a

File tree

2 files changed

+50
-21
lines changed

2 files changed

+50
-21
lines changed

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

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.amazonaws.services.lambda.runtime.Context
44
import com.amazonaws.services.lambda.runtime.RequestHandler
55
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
66
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
7+
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
78
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
89
import com.github.mduesterhoeft.router.ProtoBufUtils.toJsonWithoutWrappers
910
import com.google.common.net.MediaType
@@ -29,16 +30,16 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
2930
log.debug("match result for route '$routerFunction' is '$matchResult'")
3031
if (matchResult.match) {
3132
val handler: HandlerFunction<Any, Any> = routerFunction.handler
32-
val requestBody = deserializeRequest(handler, input)
33-
val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern)
3433
return try {
34+
val requestBody = deserializeRequest(handler, input)
35+
val request = Request(input, requestBody, routerFunction.requestPredicate.pathPattern)
3536
val response = router.filter.then(handler as HandlerFunction<*, *>).invoke(request)
3637
createResponse(input, response)
37-
} catch (e: RuntimeException) {
38+
} catch (e: Exception) {
3839
when (e) {
39-
is ApiException -> createErrorResponse(input, e)
40+
is ApiException -> createApiExceptionErrorResponse(input, e)
4041
.also { log.info("Caught api error while handling ${input.httpMethod} ${input.path} - $e") }
41-
else -> createInternalServerErrorResponse(input, e)
42+
else -> createUnexpectedErrorResponse(input, e)
4243
.also { log.error("Caught exception handling ${input.httpMethod} ${input.path} - $e", e) }
4344
}
4445
}
@@ -64,7 +65,7 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
6465
private fun handleNonDirectMatch(matchResults: List<RequestMatchResult>, input: APIGatewayProxyRequestEvent): APIGatewayProxyResponseEvent {
6566
// no direct match
6667
if (matchResults.any { it.matchPath && it.matchMethod && !it.matchContentType }) {
67-
return createErrorResponse(
68+
return createApiExceptionErrorResponse(
6869
input, ApiException(
6970
httpResponseStatus = 415,
7071
message = "Unsupported Media Type",
@@ -73,7 +74,7 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
7374
)
7475
}
7576
if (matchResults.any { it.matchPath && it.matchMethod && !it.matchAcceptType }) {
76-
return createErrorResponse(
77+
return createApiExceptionErrorResponse(
7778
input, ApiException(
7879
httpResponseStatus = 406,
7980
message = "Not Acceptable",
@@ -82,15 +83,15 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
8283
)
8384
}
8485
if (matchResults.any { it.matchPath && !it.matchMethod }) {
85-
return createErrorResponse(
86+
return createApiExceptionErrorResponse(
8687
input, ApiException(
8788
httpResponseStatus = 405,
8889
message = "Method Not Allowed",
8990
code = "METHOD_NOT_ALLOWED"
9091
)
9192
)
9293
}
93-
return createErrorResponse(
94+
return createApiExceptionErrorResponse(
9495
input, ApiException(
9596
httpResponseStatus = 404,
9697
message = "Not found",
@@ -99,7 +100,7 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
99100
)
100101
}
101102

102-
open fun createErrorResponse(input: APIGatewayProxyRequestEvent, ex: ApiException): APIGatewayProxyResponseEvent =
103+
open fun createApiExceptionErrorResponse(input: APIGatewayProxyRequestEvent, ex: ApiException): APIGatewayProxyResponseEvent =
103104
APIGatewayProxyResponseEvent()
104105
.withBody(objectMapper.writeValueAsString(mapOf(
105106
"message" to ex.message,
@@ -109,14 +110,28 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
109110
.withStatusCode(ex.httpResponseStatus)
110111
.withHeaders(mapOf("Content-Type" to "application/json"))
111112

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"))
113+
open fun createUnexpectedErrorResponse(input: APIGatewayProxyRequestEvent, ex: Exception): APIGatewayProxyResponseEvent =
114+
when (ex) {
115+
is MissingKotlinParameterException ->
116+
APIGatewayProxyResponseEvent()
117+
.withBody(objectMapper.writeValueAsString(
118+
listOf(mapOf(
119+
"path" to ex.parameter.name.orEmpty(),
120+
"message" to "Missing required field",
121+
"code" to "MISSING_REQUIRED_FIELDS"
122+
))))
123+
.withStatusCode(422)
124+
.withHeaders(mapOf("Content-Type" to "application/json"))
125+
else ->
126+
APIGatewayProxyResponseEvent()
127+
.withBody(objectMapper.writeValueAsString(mapOf(
128+
"message" to ex.message,
129+
"code" to "INTERNAL_SERVER_ERROR"
130+
)))
131+
.withStatusCode(500)
132+
.withHeaders(mapOf("Content-Type" to "application/json"))
133+
}
134+
120135

121136
open fun <T> createResponse(input: APIGatewayProxyRequestEvent, response: ResponseEntity<T>): APIGatewayProxyResponseEvent {
122137
val accept = MediaType.parse(input.acceptHeader())

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@ class RequestHandlerTest {
101101
fun `should handle request with body`() {
102102

103103
val response = testRequestHandler.handleRequest(
104-
APIGatewayProxyRequestEvent()
105-
.withPath("/some")
106-
.withHttpMethod("POST")
104+
POST("/some")
107105
.withHeaders(mapOf(
108106
"Accept" to "application/json",
109107
"Content-Type" to "application/json"
@@ -159,6 +157,22 @@ class RequestHandlerTest {
159157
assert(handler.filterInvocations).isEqualTo(2)
160158
}
161159

160+
@Test
161+
fun `should handle deserialization error`() {
162+
163+
val response = testRequestHandler.handleRequest(
164+
POST("/some")
165+
.withHeaders(
166+
mapOf(
167+
"Accept" to "application/json",
168+
"Content-Type" to "application/json"
169+
)
170+
)
171+
.withBody("{}"), mockk()
172+
)!!
173+
assert(response.statusCode).isEqualTo(422)
174+
}
175+
162176
@Test
163177
fun `should handle api exception`() {
164178

0 commit comments

Comments
 (0)