https://kotlinlang.org logo
#http4k
Title
# http4k
a

Adrian Witaszak

03/14/2024, 8:19 PM
Hi. I'm trying to handle the
APIGatewayV2HTTPEvent
in the
FnHandler
but getting Reflection error
Copy code
fun EventFnHandler() = FnHandler { e: APIGatewayV2HTTPEvent, _: Context ->
    println(e.toString())
    e.toString()
}
And the error:
Copy code
{
  "errorMessage": "Kotlin reflection implementation is not found at runtime. Make sure you have kotlin-reflect.jar in the classpath",
  "errorType": "kotlin.jvm.KotlinReflectionNotSupportedError",
  "stackTrace": [
    "kotlin.jvm.internal.ClassReference.error(ClassReference.kt:88)",
    "kotlin.jvm.internal.ClassReference.isAbstract(ClassReference.kt:62)",
    "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory.create(KotlinJsonAdapter.kt:215)",
    "com.squareup.moshi.Moshi.adapter(Moshi.java:146)",
    "com.squareup.moshi.Moshi.adapter(Moshi.java:106)",
    "com.squareup.moshi.Moshi.adapter(Moshi.java:80)",
    "org.http4k.format.ConfigurableMoshi.adapterFor(ConfigurableMoshi.kt:94)",
    "org.http4k.format.ConfigurableMoshi.asA(ConfigurableMoshi.kt:98)",
    "org.http4k.serverless.AutoMarshallingFnLoader.invoke$lambda$0(AutoMarshallingFnLoader.kt:19)",
    "org.http4k.serverless.AwsLambdaEventFunction.handleRequest(AwsLambdaEventFunction.kt:15)"
  ]
}
i guess this is because of the missing adapter, but my question is how do i use it with
FnFunction
?
Or is there something more recommended then
FnHandler
?
d

dave

03/14/2024, 8:31 PM
You don't need to use that version of the function - you can just use the built in adapter for HTTP events
a

Adrian Witaszak

03/14/2024, 8:32 PM
ApiGatewayV2LambdaFunction
?
d

dave

03/14/2024, 8:32 PM
yep 🙂
We provide the moshi adapters for the non-http events
a

Adrian Witaszak

03/14/2024, 9:02 PM
yeah i was trying this approach earlier then i changed it.. So this is what i have now:
Copy code
@KotshiJsonAdapterFactory
private object LambdaJsonAdapterFactory : JsonAdapter.Factory by LambdaJsonAdapterFactory

internal val moshi = ConfigurableMoshi(
    Moshi.Builder()
        .add(LambdaJsonAdapterFactory)
        .add(ListAdapter)
        .add(MapAdapter)
        .asConfigurable()
        .withStandardMappings()
        .done()
)

@JsonSerializable
data class Event(val message: String) {
    companion object {
        val lens = moshi.autoBody<Event>().toLens()
        val sample = Event(message = "Hello, world!")
    }
}

val eventLens = moshi.autoBody<Event>().toLens()

internal object GetHello : AppLoader {
    override fun invoke(env: Map<String, String>): HttpHandler = { request ->
        val event = eventLens(request)
            .asResultOr { return@asResultOr Response(Status.BAD_REQUEST).body("Invalid request") }
            .mapFailure { Response(Status.BAD_REQUEST).with(lens of sample) }
            .map { it.message }
            .get()
        Response(Status.OK).body(event.toString())
    }
}

@Suppress("unused")
class Handler : ApiGatewayV2LambdaFunction(GetHello)
but aws still have an issue with it:
Copy code
{
  "errorMessage": "method is invalid",
  "errorType": "java.lang.IllegalStateException",
  "stackTrace": [
    "org.http4k.serverless.ApiGatewayV2AwsHttpAdapter.toHttp4kRequest(ApiGatewayV2.kt:48)",
    "org.http4k.serverless.ApiGatewayV2AwsHttpAdapter.invoke(ApiGatewayV2.kt:58)",
    "org.http4k.serverless.ApiGatewayV2AwsHttpAdapter.invoke(ApiGatewayV2.kt:45)",
    "org.http4k.serverless.ApiGatewayFnLoader.invoke$lambda$0(ApiGatewayFnLoader.kt:24)",
    "org.http4k.serverless.AwsLambdaEventFunction.handleRequest(AwsLambdaEventFunction.kt:15)"
  ]
}
d

dave

03/14/2024, 9:03 PM
if the method is invalid it means that the input message is not in the v2 payload format. what's your aws config like for the function?
a

Adrian Witaszak

03/14/2024, 9:04 PM
all default, it is a newly created Lambda, just uploaded the Jar and typed in the Handler name
d

dave

03/14/2024, 9:05 PM
then it's v1.
a

Adrian Witaszak

03/14/2024, 9:06 PM
alright i didn't know what is the difference between v1 and v2..
d

dave

03/14/2024, 9:06 PM
in pulumi, the config is this:
Copy code
const lambdaIntegration = new aws.apigatewayv2.Integration("hello-http4k-api-lambda-integration", {
    apiId: api.id,
    integrationType: "AWS_PROXY",
    integrationUri: lambdaFunction.arn,
    payloadFormatVersion: "2.0"
});
v1 format doesn't support multiple headers with the same name
always use v2 🙂
a

Adrian Witaszak

03/14/2024, 9:42 PM
Something must be wrong with the moshi here, as if i run it from main i get
Copy code
Caused by: java.lang.NullPointerException: Cannot invoke "LambdaJsonAdapterFactory.create(java.lang.reflect.Type, java.util.Set, com.squareup.moshi.Moshi)" because "this.$$delegate_0" is null
	at LambdaJsonAdapterFactory.create(Handler.kt)
i got this one, it was a moshi issue. The fun main runs ok. But still have an issue with the AWS..
Copy code
{
  "errorMessage": "method is invalid",
  "errorType": "java.lang.IllegalStateException",
  "stackTrace": [
    "org.http4k.serverless.ApiGatewayV2AwsHttpAdapter.toHttp4kRequest(ApiGatewayV2.kt:48)",
    "org.http4k.serverless.ApiGatewayV2AwsHttpAdapter.invoke(ApiGatewayV2.kt:58)",
    "org.http4k.serverless.ApiGatewayV2AwsHttpAdapter.invoke(ApiGatewayV2.kt:45)",
    "org.http4k.serverless.ApiGatewayFnLoader.invoke$lambda$0(ApiGatewayFnLoader.kt:24)",
    "org.http4k.serverless.AwsLambdaEventFunction.handleRequest(AwsLambdaEventFunction.kt:15)"
  ]
}
but i leave it for tomorrow, it is getting late.
Isn't the other difference between v1 and v2 that one is Rest API the other Http API?
d

dave

03/15/2024, 9:03 AM
the rest API is also legacy really
V2 HTTP is where it's at unless you want to deal with the (quite frankly unwieldy) Api gateway open api specifications.
if you just want to put an HTTP api on the web then v2 HTTP it should be 🙂
a

Adrian Witaszak

03/15/2024, 9:15 AM
i will use this lambda with private api gateways only, inside vpc, so shouldn't i go back to the previous approach with the FnHandler?
d

dave

03/15/2024, 9:56 AM
the format is the same
the fact that it's API gateway vs non is only how it's exposed
k

Krisztian Balla

03/15/2024, 10:35 AM
v2 api gateway is unfortunately not an option when your services are deployed inside a vpc
d

dave

03/15/2024, 10:38 AM
This example shows the APIGateway isn't really a piece of tech here - we're referring to the JSON format. you can use HTTP functions without hooking up the APIG.. Example: https://github.com/http4k/examples/tree/master/aws-lambda-url
k

Krisztian Balla

03/15/2024, 11:39 AM
Just to make sure we are talking about the same thing, we had the following in mind (which is what we already do but using typescript/nodejs lambdas: • deploy lambda handlers individually to serve a single apig request • lambda handlers are essentially
fun apig-event -> apig-response
• routing happens on the apig level so lambdas themselves are really lean only wrapping a single use case • almost all microservices (a bunch of lambdas triggered by either apig / sqs / other aws events) are deployed inside a vpc hence using the restapi (v1 apig) but lambdas expected to be triggered that way are normalised so the events are the same in the v2 format (multivalue headers etc...) What is the recommended way to go at it using http4k in your opinion?
d

dave

03/15/2024, 11:43 AM
we always deploy our entire applications as a single lambda function - that's the beauty of the model - you're not limited to a single use-case
❤️ 1
so the apps are completely portable - you can run them locally, in memory, as a k8s service or as a lambda - the model doesn't care because the runtime is a separate layer to your actual app.
❤️ 1
but you can easily do it the other way around if you really want to - have many single endpoints and combine them into a single application for testing locally.
personally we find that the hassle of deployment setup makes us want to just use a single lambda or app runner for everything
❤️ 1
k

Krisztian Balla

03/15/2024, 11:47 AM
deployment was historically simple when using the serverless framework (migrating to cdk though but different topic 😅) but you do end up with smaller functions which spin up faster
d

dave

03/15/2024, 11:50 AM
we've found that the spin-up time is mostly affected by the bootstrapping of Jackson etc. @Andrew O'Hara has done a lot of playing around with this stuff so is in a good place to comment.
👀 1
k

Krisztian Balla

03/15/2024, 11:51 AM
do I understand well that when using the AppLoader/invoke approach you use the http4k Request/Response types? how do the lambda events map to that one? for example requestContext.authorizer / pathParameters / queryStringParameters...
d

dave

03/15/2024, 11:51 AM
binary size definitely makes a difference, but we don't think you're gaining much if your apps aren't bloated to begin with
if you're really worried, then you shouldn't be using jackson - use Moshi with generated adapters to avoid bringing in the Kotlin reflect JAR
of course, spring vs http4k is a different matter... 😉 (when it comes to number of dependencies)
k

Krisztian Balla

03/15/2024, 11:53 AM
yes most definitely, using jackson was out of the question from the get go
d

dave

03/15/2024, 11:53 AM
well are you including using the Jackson that is bundled inside the Lambda runtime? 🙂
k

Krisztian Balla

03/15/2024, 11:53 AM
but what I'd like to replace approach by our Kotlin team was springboot + jackson apps wrapped as a lambda proxy
d

dave

03/15/2024, 11:55 AM
personally I believe that if you combine http4k with http4k-connect and moshi and snapstart - that's the sweetspot
💯 1
once again, @Andrew O'Hara has experience in direct comparison. In fact - if you come to KotlinConf then he's going to be talking about it
k

Krisztian Balla

03/15/2024, 12:01 PM
Yes the target is exactly that http4k/connect/moshi/snapstart/arrowKt
a

Andrew O'Hara

03/15/2024, 2:48 PM
Yeah my KotlinConf talk is about optimizing Lambdas for cold-start performance. SnapStart can get a Spring/Jackson app most of the way to acceptable numbers, but to get the best numbers the architecture you and @dave are talking about is the way to go. Just one thing to clarify:
moshi-kotlin
isn't good enough. You need to replace the kotlin module with a generated
kotshi
adapter to eliminate
kotlin-reflect
. If you're interested, code and slides linked below. Raw data is at the end of the slides. https://github.com/oharaandrew314/have-your-serverless-kotlin-functions
👍 1
k

Krisztian Balla

03/15/2024, 2:56 PM
is there a way to access
requestContext.authorizer
using a http4k Request? We managed to make the
AppLoader
inside
ApiGatewayV1LambdaFunction
approach work but it looks like a lot of the event information including the
authorizer
is lost along the way.
d

dave

03/15/2024, 3:02 PM
@Krisztian Balla there are different constructors for the lambdas - one of them passes through the context
k

Krisztian Balla

03/15/2024, 4:27 PM
you mean this?
AppLoaderWithContexts
in the code I don't seem to find anything related to mapping of
event.requestContext
to it. However managed to make it work this way:
Copy code
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
import org.http4k.base64Decoded
import org.http4k.serverless.AwsLambdaEventFunction
import org.http4k.serverless.FnHandler
import org.http4k.serverless.FnLoader

fun eventFnHandler() = FnHandler { event: APIGatewayProxyRequestEvent, _: Context ->
    val b = if (event.isBase64Encoded) event.body.base64Decoded() else event.body
    APIGatewayProxyResponseEvent().apply {
        statusCode=201
        body=b
        isBase64Encoded=false
    }
}

fun eventFnLoader() = FnLoader { _: Map<String, String> -> eventFnHandler() }

@Suppress("unused")
class Handler : AwsLambdaEventFunction(eventFnLoader())
d

dave

03/15/2024, 4:41 PM
ah - sorry - I thought you meant the lambda context object in
com.amazonaws.services.lambda.runtime
.,
You can get access to it with something like
Copy code
object Foo : ApiGatewayV2LambdaFunction(AppLoaderWithContexts { env, context ->
    val key = RequestContextKey.required<Map<String, Any>>(context, LAMBDA_REQUEST_KEY)
    ({ req: Request ->
        val context = key[req]
        Response(Status.OK)
    })
})
the LAMBDA_REQUEST_KEY is a map in the case of AWS lambda
👀 1
k

Krisztian Balla

03/15/2024, 5:23 PM
that way of accessing the raw request does not seem to work as it is resulting in the following error 👀
Copy code
java.lang.ClassCastException: class org.http4k.core.MemoryRequest cannot be cast to class java.util.Map (org.http4k.core.MemoryRequest is in unnamed module of loader com.amazonaws.services.lambda.runtime.api.client.CustomerClassLoader @4dd8dc3; java.util.Map is in module java.base of loader 'bootstrap')
	at Handler$1$1.invoke(Handler.kt:48)
	at Handler$1$1.invoke(Handler.kt:47)
	at org.http4k.serverless.CommonKt$AddLambdaContextAndRequest$1$1.invoke(common.kt:14)
	at org.http4k.serverless.CommonKt$AddLambdaContextAndRequest$1$1.invoke(common.kt:11)
	at org.http4k.filter.ServerFilters$InitialiseRequestContext$invoke$1$1.invoke(ServerFilters.kt:375)
	at org.http4k.filter.ServerFilters$InitialiseRequestContext$invoke$1$1.invoke(ServerFilters.kt:372)
	at org.http4k.filter.ServerFilters$CatchAll$invoke$2$1.invoke(ServerFilters.kt:314)
	at org.http4k.filter.ServerFilters$CatchAll$invoke$2$1.invoke(ServerFilters.kt:312)
	at org.http4k.serverless.ApiGatewayFnLoader.invoke$lambda$0(ApiGatewayFnLoader.kt:27)
	at org.http4k.serverless.AwsLambdaEventFunction.handleRequest(AwsLambdaEventFunction.kt:15)
	at com.amazonaws.services.lambda.runtime.api.client.EventHandlerLoader$2.call(EventHandlerLoader.java:905)
	at com.amazonaws.services.lambda.runtime.api.client.AWSLambda.startRuntime(AWSLambda.java:245)
	at com.amazonaws.services.lambda.runtime.api.client.AWSLambda.startRuntime(AWSLambda.java:197)
	at com.amazonaws.services.lambda.runtime.api.client.AWSLambda.main(AWSLambda.java:187)
d

dave

03/15/2024, 6:14 PM
ah sorry- I was wrong. Fall back on your current version then I suppose 🙃
👍 1
8 Views