Hi. I'm trying to handle the `APIGatewayV2HTTPEven...
# http4k
a
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
You don't need to use that version of the function - you can just use the built in adapter for HTTP events
a
ApiGatewayV2LambdaFunction
?
d
yep 🙂
We provide the moshi adapters for the non-http events
a
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
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
all default, it is a newly created Lambda, just uploaded the Jar and typed in the Handler name
d
then it's v1.
a
alright i didn't know what is the difference between v1 and v2..
d
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
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
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
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
the format is the same
the fact that it's API gateway vs non is only how it's exposed
k
v2 api gateway is unfortunately not an option when your services are deployed inside a vpc
d
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
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
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
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
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
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
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
yes most definitely, using jackson was out of the question from the get go
d
well are you including using the Jackson that is bundled inside the Lambda runtime? 🙂
k
but what I'd like to replace approach by our Kotlin team was springboot + jackson apps wrapped as a lambda proxy
d
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
Yes the target is exactly that http4k/connect/moshi/snapstart/arrowKt
a
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
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
@Krisztian Balla there are different constructors for the lambdas - one of them passes through the context
k
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
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
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
ah sorry- I was wrong. Fall back on your current version then I suppose 🙃
👍 1