@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory
1212import kotlin.reflect.KClass
1313import kotlin.reflect.jvm.reflect
1414
15+ @Suppress(" UnstableApiUsage" )
1516abstract class RequestHandler : RequestHandler <APIGatewayProxyRequestEvent , APIGatewayProxyResponseEvent > {
1617
1718 open val objectMapper = jacksonObjectMapper()
@@ -29,41 +30,39 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
2930 val matchResult = routerFunction.requestPredicate.match(input)
3031 log.debug(" match result for route '$routerFunction ' is '$matchResult '" )
3132 if (matchResult.match) {
33+ val matchedAcceptType = routerFunction.requestPredicate.matchedAcceptType(input.acceptedMediaTypes())
34+ ? : MediaType .parse(router.defaultContentType)
3235 if (! permissionHandlerSupplier()(input).hasAnyRequiredPermission(routerFunction.requestPredicate.requiredPermissions))
33- return createApiExceptionErrorResponse(input, ApiException (" unauthorized" , " UNAUTHORIZED" , 401 ))
36+ return createApiExceptionErrorResponse(matchedAcceptType, input, ApiException (" unauthorized" , " UNAUTHORIZED" , 401 ))
3437
3538 val handler: HandlerFunction <Any , Any > = routerFunction.handler
3639 return try {
3740 val requestBody = deserializeRequest(handler, input)
3841 val request =
3942 Request (input, requestBody, routerFunction.requestPredicate.pathPattern)
4043 val response = router.filter.then(handler as HandlerFunction <* , * >).invoke(request)
41- createResponse(routerFunction.requestPredicate. matchedAcceptType(input.acceptHeader()) , input, response)
44+ createResponse(matchedAcceptType, input, response)
4245 } catch (e: Exception ) {
4346 when (e) {
44- is ApiException -> createApiExceptionErrorResponse(input, e)
47+ is ApiException -> createApiExceptionErrorResponse(matchedAcceptType, input, e)
4548 .also { log.info(" Caught api error while handling ${input.httpMethod} ${input.path} - $e " ) }
46- else -> createUnexpectedErrorResponse(input, e)
49+ else -> createUnexpectedErrorResponse(matchedAcceptType, input, e)
4750 .also { log.error(" Caught exception handling ${input.httpMethod} ${input.path} - $e " , e) }
4851 }
4952 }
5053 }
5154
5255 matchResult
5356 }
54- return handleNonDirectMatch(matchResults, input)
57+ return handleNonDirectMatch(MediaType .parse(router.defaultContentType), matchResults, input)
5558 }
5659
5760 open fun serializationHandlers (): List <SerializationHandler > = listOf (
58- JsonSerializationHandler (
59- objectMapper
60- )
61+ JsonSerializationHandler (objectMapper)
6162 )
6263
6364 open fun deserializationHandlers (): List <DeserializationHandler > = listOf (
64- JsonDeserializationHandler (
65- objectMapper
66- )
65+ JsonDeserializationHandler (objectMapper)
6766 )
6867
6968 open fun permissionHandlerSupplier (): (r: APIGatewayProxyRequestEvent ) -> PermissionHandler =
@@ -80,89 +79,87 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
8079 }
8180 }
8281
83- private fun handleNonDirectMatch (matchResults : List <RequestMatchResult >, input : APIGatewayProxyRequestEvent ): APIGatewayProxyResponseEvent {
82+ private fun handleNonDirectMatch (defaultContentType : MediaType , matchResults : List <RequestMatchResult >, input : APIGatewayProxyRequestEvent ): APIGatewayProxyResponseEvent {
8483 // no direct match
85- if (matchResults.any { it.matchPath && it.matchMethod && ! it.matchContentType }) {
86- return createApiExceptionErrorResponse(
87- input, ApiException (
88- httpResponseStatus = 415 ,
89- message = " Unsupported Media Type" ,
90- code = " UNSUPPORTED_MEDIA_TYPE"
84+ val apiException =
85+ when {
86+ matchResults.any { it.matchPath && it.matchMethod && ! it.matchContentType } ->
87+ ApiException (
88+ httpResponseStatus = 415 ,
89+ message = " Unsupported Media Type" ,
90+ code = " UNSUPPORTED_MEDIA_TYPE"
91+ )
92+ matchResults.any { it.matchPath && it.matchMethod && ! it.matchAcceptType } ->
93+ ApiException (
94+ httpResponseStatus = 406 ,
95+ message = " Not Acceptable" ,
96+ code = " NOT_ACCEPTABLE"
97+ )
98+ matchResults.any { it.matchPath && ! it.matchMethod } ->
99+ ApiException (
100+ httpResponseStatus = 405 ,
101+ message = " Method Not Allowed" ,
102+ code = " METHOD_NOT_ALLOWED"
91103 )
92- )
93- }
94- if (matchResults.any { it.matchPath && it.matchMethod && ! it.matchAcceptType }) {
95- return createApiExceptionErrorResponse(
96- input, ApiException (
97- httpResponseStatus = 406 ,
98- message = " Not Acceptable" ,
99- code = " NOT_ACCEPTABLE"
104+ else -> ApiException (
105+ httpResponseStatus = 404 ,
106+ message = " Not found" ,
107+ code = " NOT_FOUND"
100108 )
101- )
102- }
103- if (matchResults.any { it.matchPath && ! it.matchMethod }) {
104- return createApiExceptionErrorResponse(
105- input, ApiException (
106- httpResponseStatus = 405 ,
107- message = " Method Not Allowed" ,
108- code = " METHOD_NOT_ALLOWED"
109+ }
110+ val contentType = input.acceptedMediaTypes().firstOrNull() ? : defaultContentType
111+ return createApiExceptionErrorResponse(contentType, input, apiException)
112+ }
113+
114+ /* *
115+ * Customize the format of an api error
116+ */
117+ open fun createErrorBody (error : ApiError ): Any = error
118+
119+ /* *
120+ * Customize the format of an unprocessable entity error
121+ */
122+ open fun createUnprocessableEntityErrorBody (error : UnprocessableEntityError ): Any =
123+ error
124+
125+ open fun createApiExceptionErrorResponse (contentType : MediaType , input : APIGatewayProxyRequestEvent , ex : ApiException ): APIGatewayProxyResponseEvent =
126+ createErrorBody(ex.toApiError()).let {
127+ APIGatewayProxyResponseEvent ()
128+ .withBody(
129+ // in case of 406 we might find no serializer so fall back to the default
130+ if (serializationHandlerChain.supports(contentType, it))
131+ serializationHandlerChain.serialize(contentType, it)
132+ else
133+ serializationHandlerChain.serialize(MediaType .parse(router.defaultContentType), it)
109134 )
110- )
135+ .withStatusCode(ex.httpResponseStatus)
136+ .withHeaders(mapOf (" Content-Type" to contentType.toString()))
111137 }
112- return createApiExceptionErrorResponse(
113- input, ApiException (
114- httpResponseStatus = 404 ,
115- message = " Not found" ,
116- code = " NOT_FOUND"
117- )
118- )
119- }
120138
121- open fun createApiExceptionErrorResponse (input : APIGatewayProxyRequestEvent , ex : ApiException ): APIGatewayProxyResponseEvent =
122- APIGatewayProxyResponseEvent ()
123- .withBody(objectMapper.writeValueAsString(mapOf (
124- " message" to ex.message,
125- " code" to ex.code,
126- " details" to ex.details
127- )))
128- .withStatusCode(ex.httpResponseStatus)
129- .withHeaders(mapOf (" Content-Type" to " application/json" ))
130-
131- open fun createUnexpectedErrorResponse (input : APIGatewayProxyRequestEvent , ex : Exception ): APIGatewayProxyResponseEvent =
139+ open fun createUnexpectedErrorResponse (contentType : MediaType , input : APIGatewayProxyRequestEvent , ex : Exception ): APIGatewayProxyResponseEvent =
132140 when (ex) {
133141 is MissingKotlinParameterException ->
134- APIGatewayProxyResponseEvent ()
135- .withBody(objectMapper.writeValueAsString(
136- listOf (mapOf (
137- " path" to ex.parameter.name.orEmpty(),
138- " message" to " Missing required field" ,
139- " code" to " MISSING_REQUIRED_FIELDS"
140- ))))
141- .withStatusCode(422 )
142- .withHeaders(mapOf (" Content-Type" to " application/json" ))
143- else ->
144- APIGatewayProxyResponseEvent ()
145- .withBody(objectMapper.writeValueAsString(mapOf (
146- " message" to ex.message,
147- " code" to " INTERNAL_SERVER_ERROR"
148- )))
149- .withStatusCode(500 )
150- .withHeaders(mapOf (" Content-Type" to " application/json" ))
142+ createResponse(contentType, input,
143+ ResponseEntity (422 , createUnprocessableEntityErrorBody(UnprocessableEntityError (
144+ message = " Missing required field" ,
145+ code = " MISSING_REQUIRED_FIELDS" ,
146+ path = ex.parameter.name.orEmpty()))))
147+ else -> createResponse(contentType, input,
148+ ResponseEntity (500 , createErrorBody(ApiError (ex.message.orEmpty(), " INTERNAL_SERVER_ERROR" ))))
151149 }
152150
153151 open fun <T > createResponse (contentType : MediaType ? , input : APIGatewayProxyRequestEvent , response : ResponseEntity <T >): APIGatewayProxyResponseEvent {
154- // TODO add default accept type
155152 return when {
156153 // no-content response
157- response.body == null && response.statusCode == 204 -> APIGatewayProxyResponseEvent ()
158- .withStatusCode(204 )
154+ response.body == null -> APIGatewayProxyResponseEvent ()
155+ .withStatusCode(response.statusCode )
159156 .withHeaders(response.headers)
160- serializationHandlerChain.supports(contentType!! , response) ->
157+ serializationHandlerChain.supports(contentType!! , response.body ) ->
161158 APIGatewayProxyResponseEvent ()
162159 .withStatusCode(response.statusCode)
163- .withBody(serializationHandlerChain.serialize(contentType, response))
160+ .withBody(serializationHandlerChain.serialize(contentType, response.body ))
164161 .withHeaders(response.headers + (" Content-Type" to contentType.toString()))
165- else -> throw IllegalArgumentException (" unsupported response $response " )
162+ else -> throw IllegalArgumentException (" unsupported response ${ response.body. let { (it as Any ):: class .java }} and $contentType " )
166163 }
167164 }
168165
0 commit comments