Repository: awslabs/aws-lambda-rust-runtime (language: rust)

Issue:
Improving Compile Times And/Or Add Pluggable Deserializable Types
Hey! First let me say that I love this project. Clearly a lot of thoughtful design work has been put in. Kudos to all the contributors. Sorry about the wall of text in advance.

My scenario is this: I’ve noticed that even debug incremental recompiles of an absolutely barebones `lambda_http` app take upwards of 15 seconds. That doesn’t sound like much but multiply this by 10x lambdas in a workspace and it gets out of hand pretty quick.

I did some cursory investigation (the results of which I can post later). My investigation indicates that the slowness comes during the `codegen_crate` and subsequent LLVM optimize steps of the build.

Checking the amount of emitted LLVM IR, I see that it’s in the 4M+ lines range! This is a lot of code for LLVM to optimize, so it explains why both the steps take so long.

I took a look at why the IR is being generated but I couldn’t find any obvious pathological cases. It appears that the majority of the IR is `serde::Deserialize` impls on the `aws_lambda_events` types used by this crate.

I took a look at those types, and they have a heck of a lot of `#[serde(deserialize_with=“…”)]` on their fields. That attribute isn’t too expensive on its own, but multiplied across every field of every struct -- that’s a lot of codegen. There was also some duplicate code because `#[derive(Deserialize)]` implements `visit_seq` **and** `visit_map`. The former is unnecessary because we’re dealing with JSON and it is strictly invalid in most cases to allow users to deserialize a JSON array into a struct.

(Side-note: there was a proposal to [share the `deserialize_with` wrappers between `visit_seq` and `visit_map`](https://github.com/serde-rs/serde/issues/420), but it was closed as `wontfix`)

So I just cut the `visit_seq` code out entirely via a custom `#[derive(Deserialize)]` impl. That plus stripping down this crate’s `LambdaRequest` enum to only cover the cases I care about let me bring the LLVM IR count down from 4.5M to about 3.5.

That was it for the low-hanging fruit. The remaining deserialization code seems necessary, but is a bit bloated, purely because serde is very generous with error handling logic. I could probably write it out by hand to be more efficient, or improve the existing codegen of the `#[derive(Deserialize)]` macro.

But I’ve been rethinking my approach. **The problem is that we’re re-running the `#[derive(Deserialize)]` macro every time the lambda binary compiles.** There are some efforts to [disable derives when running cargo check](https://github.com/async-graphql/async-graphql/issues/783#issuecomment-1067460045). This is clever for the purposes of improving type hint resolution times, etc. But I don’t think it’s sufficient. What’s needed is a true way to cache the results of proc macro evaluations -- if said proc macros are pure. This finally brings me to my first question: **Does anyone know of a way to cache the results of a proc-macro evaluation between builds?**

Alternatively: **Is it possible to force rustc to evaluate a proc macro for a struct, *before* said struct is used in a leaf crate?**

cc @LegNeato  @calavera as maintainers of the aws-lambda-events crate, any ideas? Perhaps we could **Selectively cargo-expand the `Deserialize` impls in the aws-lambda-events crate?** It would result in a much larger source file, but could cut out at least some of the `crate_codegen` time in incremental builds. Not sure how much but it’d be a decent chunk of the 40% it seems to take now.

cc @calavera @nmoutschen as maintainers of this crate, any ideas? Perhaps we could refactor things a bit so that users can provide their own APIGW types and deserialize on their end? That way there’s a bit more flexibility. For example I could fork aws-lambda-events, strip it to just the types I need, and hand roll `Deserialize` impls. At the moment I’m locked in to the types this crate provides. I understand that this crate does this because it handles a lot of the boilerplate when it comes to talking to AWS, but I’m fine with my crate handling some of that.





Discussion / hints:
Another option is to look at crates like https://github.com/dtolnay/miniserde or https://github.com/not-fl3/nanoserde. Disabling the deserializer derives on the `aws-lambda-events` types and replacing them with a hand-rolled nanoserde impl is probably the dream in terms of compile times, but I realize that this trades compile times for development effort on behalf of the crate authors. So unless it could be done with codegen at the time the dependencies are built, it's probably not viable.
I definitely agree with you. The reason why `aws_lambda_events` have so many deserialization attributes is because there is not a canonical source of truth that really defines if fields are required or optional, so we have to try to cover every single scenario that we can think of. I know AWS is working on improving this, but it's not an easy task since it requires coordination from every service team that publishes lambda events.

> Perhaps we could refactor things a bit so that users can provide their own APIGW types and deserialize on their end?

You can already do this by using the lambda_runtime directly. If you don't care about about having an `http::Request` object, you can just do this:

```
use lambda_runtime::{service_fn, Error, LambdaEvent};

pub(crate) async fn my_handler(event: LambdaEvent<YourOwnAPIGWRequestStruct>) -> Result<(YourOwnAPIGWResponseStruct, Error> {
  let object = event.payload.;

  // Do something useful with the object here
  
  let resp = ...
  Ok(resp)
}
```

Feel free to bring ideas and suggestions to the `aws_lambda_events` repository. I'll really appreciate your help to make the codebase better.
Something else to point out, is that the apigw events in `aws_lambda_events` are no longer generated using the Go lambda event structures, so they can be optimized as much as we want 😉 
> You can already do this by using the lambda_runtime directly. If you don't care about about having an http::Request object, you can just do this:

Brilliant! Somehow I missed that possibility. I'll take a look to see how much that helps. Thank you.

> Something else to point out, is that the apigw events in aws_lambda_events are no longer generated using the Go lambda event structures, so they can be optimized as much as we want 😉

I didn't know that, that's great. Should provide a lot more flexibility when it comes to optimizing.

> I definitely agree with you. The reason why aws_lambda_events have so many deserialization attributes is because there is not a canonical source of truth that really defines if fields are required or optional, so we have to try to cover every single scenario that we can think of. I know AWS is working on improving this, but it's not an easy task since it requires coordination from every service team that publishes lambda events.

That's a shame. Is there any way `aws-lambda-events` could become the source of truth? Is it possible to make every field mandatory, then turn them optional over time, as we find out which fields are required? It'd be a major breaking API change. That gives me another, possibly insane idea...

The `aws-lambda-events` crate has features to enable/disable types per-service. Could make that more granular? Down to a per-struct or per-field level? That way users bring in only as much serialization code as they require.

A more sane option would be to make the `serde` dependency in `aws-lambda-events` optional. Users can continue using the crate as normal, with no breaking changes. Users who want to optimize for compile times can disable this feature. They can then implement serialization manually, for the types they use, via [remote derive](https://serde.rs/remote-derive.html). The benefit is that users don't need to re-implement the types. `aws-lambda-events` remains the source of truth for type definitions. Users just opt-in to manual serialization/deserialization.

> Feel free to bring ideas and suggestions to the aws_lambda_events repository. I'll really appreciate your help to make the codebase better.

Absolutely, happy to help. Modifying `serde_derive` to add an attribute that makes `visit_seq` optional is a good first step. This is the relevant issue upstream: https://github.com/serde-rs/serde/issues/1587. I'll open a PR with my implementation of it. Once that's merged we can enable the attribute on the fields in `aws-lambda-types`.

P.S. For anyone who is curious on digging into this for themselves, here are some tangential issues/posts I found while investigating the problem. Most of them aren't actual *bugs* per se but they include **very useful scripts/information on how to profile** this stuff:
- https://github.com/serde-rs/json/issues/313
- https://github.com/serde-rs/serde/issues/286
    - especially [this comment](https://github.com/serde-rs/serde/issues/286#issuecomment-279325284) by dtolnay
- https://github.com/serde-rs/serde/issues/1146
    - especially [this comment](https://github.com/serde-rs/serde/issues/1146#issuecomment-907622649) by matklad
- https://github.com/dtolnay/cargo-llvm-lines/issues/7
    - especially [this comment](https://github.com/dtolnay/cargo-llvm-lines/issues/7#issuecomment-907766439) by matklad
- https://github.com/servo/servo/issues/26713
- https://github.com/servo/servo/pull/27063#issuecomment-649040771
    - especially [this comment](https://github.com/servo/servo/pull/27063#issuecomment-649040771) ZoeyR on they cut the serialization code in bincode by a factor of 4x
- https://github.com/rust-lang/rust-analyzer/issues/10065
    - especially [this referenced zulip chat](https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Frust-analyzer/topic/Dropping.20arcs) that explains how the rust-analyzer team discovered and fixed an issue that caused hundreds of duplicate monomorphizations

There are also some existing PRs to serde/serde_json by Marwes/dtolnay that might help with reducing the amount of generated IR, if merged:
- https://github.com/serde-rs/json/pull/687
- https://github.com/serde-rs/json/pull/690
- https://github.com/serde-rs/json/pull/745
- https://github.com/serde-rs/serde/pull/1848
cc @matklad. It seems you were on a similar crusade last year. I'm curious to know what conclusions you came to regarding speeding up `serde_derive` usage.
Yeah, in rust-analyzer we also noticed that serde adds a lot of code. For us, that’s not a problem as this happens in the leaf crate, not in the API crate.

> Alternatively: Is it possible to force rustc to evaluate a proc macro for a struct, before said struct is used in a leaf crate?

I think proc macros are evaluated once, eagerly, in the crate which defines the struct. But if proc macro generates a bunch of generic functions, those fns than are monimorphised lazily, with duplication across leaf crates. So another avenue for exploration is to find a trick to force monomorohisstion happen upstream somehow, something along these lines

```
#[derive(Serialize)]
pub struct Foo {…}

pub fn Foo2json(foo: &Foo) -> String {…}
```

If the consumer of the above crate used Foo via Serialize interface, compile times will suffer. Using Foo2json non-generic function should be fine though!
Also, I feel that "JSON serialization takes a lot time to compile" is a problem with hits pretty-much the whole Rust web services ecosystem and, subjectively, it has smaller visibility than it deserves. 

So I am wondering if some t-compiler folks at AWS might want to take a look here? cc @pnkfelix
Before we go though the route of big optimizations, a short term, specific, work that we can do, is to identify exactly which fields are required in those events, and which are optional, and remove as much serde attributes as possible. The reason why there are so many is because the Rust events are based on the Go events, and the language semantics are very different. See this issue for example: 

https://github.com/LegNeato/aws-lambda-events/issues/22

We could look into other event packages, like the Java one, which might be more stricts, and strip as many serde attributes as possible. 
Sorry I've been MIA. I'd love to see what the dumbest / straightforward path of just expanding the macros and checking in the generated code before we get fancy.
> My scenario is this: I’ve noticed that even debug incremental recompiles of an absolutely barebones `lambda_http` app take upwards of 15 seconds. That doesn’t sound like much but multiply this by 10x lambdas in a workspace and it gets out of hand pretty quick.

hey @indiv0, I got curious this morning, so I took a look at how long it takes to compile for me locally. I noticed that for a simple app, we're bringing all the features from Tokio and Tower, which might not be necessary. The codegen phase for aws_lambda_events is only about 2 seconds on my laptop. There is definitely room for improvement, and we might be able to reduce compilation time with some adjustments in the dependencies that we're bringing. Can you share your cargo timings information so I can take a look?

I put my timings output in this URL so you can take a look: https://starlit-buttercream-cf1ff0.netlify.app/
You can generate a similar one with `cargo lambda build --release --timings`.
I did a basic experiment over the weekend to measure how much time `aws_lambda_events` adds to the compilation of basic http functions. I removed all the fields from the `apigw` and `alb` events, and compilation time for a function already compiled dropped from 18 seconds to 9 seconds. This is a significant drop that we should try to fix, however, I don't believe we can just save 9 seconds, after all, I removed all the information from the events.

These are some ideas that we could explore:

1. Separate serialization from deserialization: Right now events include both attributes `Serialize`, and `Deserialize`, even though request events only need `Deserialize`, and response events only need `Serialize`.
2. Remove as many `deserialize_with` as possible.
3. Implement `Serialize`, and `Deserialize` manually.
@calavera , this 2-way Ser/Deser is one of those bugs that were there for long enough to become a feature. [This PR](https://github.com/awslabs/aws-lambda-rust-runtime/commit/c01811a33d33a89d0c55c3c9e90916832f1f7ca8#diff-afcd8529836749ef09e0247d3d27bb30868d4387ea341f7a003a7c2b216070faR37) added the same for _Context_ to enable stubbing out the lambda and running it locally via an SQS proxy - the payload itself already had both.

If we are to make it one way only, it would be great to still have the other way as an optional feature. A few of my clients rely on it for development. If you do remove it we should still be able to use remote derive, but that can be painful.
> If we are to make it one way only, it would be great to still have the other way as an optional feature.

Yes, that should definitely be available. I was thinking something in the line of how Serde itself works, were you have to import `serde::de` to access de-serialization options, and `serde::ser` to access serialization options. That should probably be a discussion in the aws_lambda_events repo.

Fix patch:
diff --git a/.gitignore b/.gitignore
index 51840b6d..d8feb244 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ output.json
 
 .aws-sam
 build
+.vscode
diff --git a/README.md b/README.md
index f49ebd1e..46f87e5c 100644
--- a/README.md
+++ b/README.md
@@ -330,12 +330,6 @@ You can read more about how [cargo lambda start](https://github.com/calavera/car
 
 Lambdas can be run and debugged locally using a special [Lambda debug proxy](https://github.com/rimutaka/lambda-debug-proxy) (a non-AWS repo maintained by @rimutaka), which is a Lambda function that forwards incoming requests to one AWS SQS queue and reads responses from another queue. A local proxy running on your development computer reads the queue, calls your Lambda locally and sends back the response. This approach allows debugging of Lambda functions locally while being part of your AWS workflow. The Lambda handler code does not need to be modified between the local and AWS versions.
 
-## `lambda_runtime`
-
-`lambda_runtime` is a library for authoring reliable and performant Rust-based AWS Lambda functions. At a high level, it provides `lambda_runtime::run`, a function that runs a `tower::Service<LambdaEvent>`.
-
-To write a function that will handle request, you need to pass it through `service_fn`, which will convert your function into a `tower::Service<LambdaEvent>`, which can then be run by `lambda_runtime::run`.
-
 ## AWS event objects
 
 This project does not currently include Lambda event struct definitions. Instead, the community-maintained [`aws_lambda_events`](https://crates.io/crates/aws_lambda_events) crate can be leveraged to provide strongly-typed Lambda event structs. You can create your own custom event objects and their corresponding structs as well.
@@ -378,6 +372,30 @@ fn main() -> Result<(), Box<Error>> {
 }
 ```
 
+## Feature flags in lambda_http
+
+`lambda_http` is a wrapper for HTTP events coming from three different services, Amazon Load Balancer (ALB), Amazon Api Gateway (APIGW), and AWS Lambda Function URLs. Amazon Api Gateway can also send events from three different endpoints, REST APIs, HTTP APIs, and WebSockets. `lambda_http` transforms events from all these sources into native `http::Request` objects, so you can incorporate Rust HTTP semantics into your Lambda functions.
+
+By default, `lambda_http` compiles your function to support any of those services. This increases the compile time of your function because we have to generate code for all the sources. In reality, you'll usually put a Lambda function only behind one of those sources. You can choose which source to generate code for with feature flags.
+
+The available features flags for `lambda_http` are the following:
+
+- `alb`: for events coming from [Amazon Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/).
+- `apigw_rest`: for events coming from [Amazon API Gateway Rest APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html).
+- `apigw_http`: for events coming from [Amazon API Gateway HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) and [AWS Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html).
+- `apigw_websockets`: for events coming from [Amazon API Gateway WebSockets](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html).
+
+If you only want to support one of these sources, you can disable the default features, and enable only the source that you care about in your package's `Cargo.toml` file. Substitute the dependency line for `lambda_http` for the snippet below, changing the feature that you want to enable:
+
+```toml
+[dependencies.lambda_http]
+version = "0.5.3"
+default-features = false
+features = ["apigw_rest"]
+```
+
+This will make your function compile much faster.
+
 ## Supported Rust Versions (MSRV)
 
 The AWS Lambda Rust Runtime requires a minimum of Rust 1.54, and is not guaranteed to build on compiler versions earlier than that.
diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml
index a032a131..57a43f55 100644
--- a/lambda-http/Cargo.toml
+++ b/lambda-http/Cargo.toml
@@ -12,12 +12,14 @@ documentation = "https://docs.rs/lambda_runtime"
 categories = ["web-programming::http-server"]
 readme = "../README.md"
 
-[badges]
-travis-ci = { repository = "awslabs/aws-lambda-rust-runtime" }
-maintenance = { status = "actively-developed" }
+[features]
+default = ["apigw_rest", "apigw_http", "apigw_websockets", "alb"]
+apigw_rest = []
+apigw_http = []
+apigw_websockets = []
+alb = []
 
 [dependencies]
-aws_lambda_events = { version = "^0.6.3", default-features = false, features = ["alb", "apigw"]}
 base64 = "0.13.0"
 bytes = "1"
 http = "0.2"
@@ -28,6 +30,11 @@ serde_json = "^1"
 serde_urlencoded = "0.7.0"
 query_map = { version = "0.5", features = ["url-query"] }
 
+[dependencies.aws_lambda_events]
+version = "^0.6.3"
+default-features = false
+features = ["alb", "apigw"]
+
 [dev-dependencies]
 log = "^0.4"
 maplit = "1.0"
diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs
index d6b0678d..c6327b7e 100644
--- a/lambda-http/src/request.rs
+++ b/lambda-http/src/request.rs
@@ -4,11 +4,14 @@
 //! request extension method provided by [lambda_http::RequestExt](../trait.RequestExt.html)
 //!
 use crate::ext::{PathParameters, QueryStringParameters, RawHttpPath, StageVariables};
+#[cfg(feature = "alb")]
 use aws_lambda_events::alb::{AlbTargetGroupRequest, AlbTargetGroupRequestContext};
-use aws_lambda_events::apigw::{
-    ApiGatewayProxyRequest, ApiGatewayProxyRequestContext, ApiGatewayV2httpRequest, ApiGatewayV2httpRequestContext,
-    ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext,
-};
+#[cfg(feature = "apigw_rest")]
+use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyRequestContext};
+#[cfg(feature = "apigw_http")]
+use aws_lambda_events::apigw::{ApiGatewayV2httpRequest, ApiGatewayV2httpRequestContext};
+#[cfg(feature = "apigw_websockets")]
+use aws_lambda_events::apigw::{ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext};
 use aws_lambda_events::encodings::Body;
 use http::header::HeaderName;
 use query_map::QueryMap;
@@ -25,9 +28,13 @@ use std::{io::Read, mem};
 #[derive(Deserialize, Debug)]
 #[serde(untagged)]
 pub enum LambdaRequest {
+    #[cfg(feature = "apigw_rest")]
     ApiGatewayV1(ApiGatewayProxyRequest),
+    #[cfg(feature = "apigw_http")]
     ApiGatewayV2(ApiGatewayV2httpRequest),
+    #[cfg(feature = "alb")]
     Alb(AlbTargetGroupRequest),
+    #[cfg(feature = "apigw_websockets")]
     WebSocket(ApiGatewayWebsocketProxyRequest),
 }
 
@@ -37,9 +44,13 @@ impl LambdaRequest {
     /// type of response the request origin expects.
     pub fn request_origin(&self) -> RequestOrigin {
         match self {
+            #[cfg(feature = "apigw_rest")]
             LambdaRequest::ApiGatewayV1 { .. } => RequestOrigin::ApiGatewayV1,
+            #[cfg(feature = "apigw_http")]
             LambdaRequest::ApiGatewayV2 { .. } => RequestOrigin::ApiGatewayV2,
+            #[cfg(feature = "alb")]
             LambdaRequest::Alb { .. } => RequestOrigin::Alb,
+            #[cfg(feature = "apigw_websockets")]
             LambdaRequest::WebSocket { .. } => RequestOrigin::WebSocket,
         }
     }
@@ -50,15 +61,20 @@ impl LambdaRequest {
 #[derive(Debug)]
 pub enum RequestOrigin {
     /// API Gateway request origin
+    #[cfg(feature = "apigw_rest")]
     ApiGatewayV1,
     /// API Gateway v2 request origin
+    #[cfg(feature = "apigw_http")]
     ApiGatewayV2,
     /// ALB request origin
+    #[cfg(feature = "alb")]
     Alb,
     /// API Gateway WebSocket
+    #[cfg(feature = "apigw_websockets")]
     WebSocket,
 }
 
+#[cfg(feature = "apigw_http")]
 fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Body> {
     let http_method = ag.request_context.http.method.clone();
     let raw_path = ag.raw_path.unwrap_or_default();
@@ -130,6 +146,7 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Bod
     req
 }
 
+#[cfg(feature = "apigw_rest")]
 fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
     let http_method = ag.http_method;
     let raw_path = ag.path.unwrap_or_default();
@@ -196,6 +213,7 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
     req
 }
 
+#[cfg(feature = "alb")]
 fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
     let http_method = alb.http_method;
     let raw_path = alb.path.unwrap_or_default();
@@ -261,6 +279,7 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
     req
 }
 
+#[cfg(feature = "apigw_websockets")]
 fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request<Body> {
     let http_method = ag.http_method;
     let builder = http::Request::builder()
@@ -338,12 +357,16 @@ fn apigw_path_with_stage(stage: &Option<String>, path: &str) -> String {
 #[serde(untagged)]
 pub enum RequestContext {
     /// API Gateway proxy request context
+    #[cfg(feature = "apigw_rest")]
     ApiGatewayV1(ApiGatewayProxyRequestContext),
     /// API Gateway v2 request context
+    #[cfg(feature = "apigw_http")]
     ApiGatewayV2(ApiGatewayV2httpRequestContext),
     /// ALB request context
+    #[cfg(feature = "alb")]
     Alb(AlbTargetGroupRequestContext),
     /// WebSocket request context
+    #[cfg(feature = "apigw_websockets")]
     WebSocket(ApiGatewayWebsocketProxyRequestContext),
 }
 
@@ -351,9 +374,13 @@ pub enum RequestContext {
 impl<'a> From<LambdaRequest> for http::Request<Body> {
     fn from(value: LambdaRequest) -> Self {
         match value {
-            LambdaRequest::ApiGatewayV2(ag) => into_api_gateway_v2_request(ag),
+            #[cfg(feature = "apigw_rest")]
             LambdaRequest::ApiGatewayV1(ag) => into_proxy_request(ag),
+            #[cfg(feature = "apigw_http")]
+            LambdaRequest::ApiGatewayV2(ag) => into_api_gateway_v2_request(ag),
+            #[cfg(feature = "alb")]
             LambdaRequest::Alb(alb) => into_alb_request(alb),
+            #[cfg(feature = "apigw_websockets")]
             LambdaRequest::WebSocket(ag) => into_websocket_request(ag),
         }
     }
@@ -422,7 +449,7 @@ mod tests {
     }
 
     #[test]
-    fn deserializes_minimal_apigw_v2_request_events() {
+    fn deserializes_minimal_apigw_http_request_events() {
         // from the docs
         // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request
         let input = include_str!("../tests/data/apigw_v2_proxy_request_minimal.json");
@@ -450,7 +477,7 @@ mod tests {
     }
 
     #[test]
-    fn deserializes_apigw_v2_request_events() {
+    fn deserializes_apigw_http_request_events() {
         // from the docs
         // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request
         let input = include_str!("../tests/data/apigw_v2_proxy_request.json");
@@ -593,7 +620,7 @@ mod tests {
     }
 
     #[test]
-    fn deserialize_apigw_v2_sam_local() {
+    fn deserialize_apigw_http_sam_local() {
         // manually generated from AWS SAM CLI
         // Steps to recreate:
         // * sam init
diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs
index 4ea9c895..0c46ae68 100644
--- a/lambda-http/src/response.rs
+++ b/lambda-http/src/response.rs
@@ -2,8 +2,12 @@
 
 use crate::request::RequestOrigin;
 use aws_lambda_events::encodings::Body;
+#[cfg(feature = "alb")]
 use aws_lambda_events::event::alb::AlbTargetGroupResponse;
-use aws_lambda_events::event::apigw::{ApiGatewayProxyResponse, ApiGatewayV2httpResponse};
+#[cfg(any(feature = "apigw_rest", feature = "apigw_websockets"))]
+use aws_lambda_events::event::apigw::ApiGatewayProxyResponse;
+#[cfg(feature = "apigw_http")]
+use aws_lambda_events::event::apigw::ApiGatewayV2httpResponse;
 use http::{
     header::{CONTENT_TYPE, SET_COOKIE},
     Response,
@@ -15,8 +19,11 @@ use serde::Serialize;
 #[derive(Serialize, Debug)]
 #[serde(untagged)]
 pub enum LambdaResponse {
-    ApiGatewayV2(ApiGatewayV2httpResponse),
+    #[cfg(any(feature = "apigw_rest", feature = "apigw_websockets"))]
     ApiGatewayV1(ApiGatewayProxyResponse),
+    #[cfg(feature = "apigw_http")]
+    ApiGatewayV2(ApiGatewayV2httpResponse),
+    #[cfg(feature = "alb")]
     Alb(AlbTargetGroupResponse),
 }
 
@@ -37,6 +44,15 @@ impl LambdaResponse {
         let status_code = parts.status.as_u16();
 
         match request_origin {
+            #[cfg(feature = "apigw_rest")]
+            RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse {
+                body,
+                status_code: status_code as i64,
+                is_base64_encoded: Some(is_base64_encoded),
+                headers: headers.clone(),
+                multi_value_headers: headers,
+            }),
+            #[cfg(feature = "apigw_http")]
             RequestOrigin::ApiGatewayV2 => {
                 // ApiGatewayV2 expects the set-cookies headers to be in the "cookies" attribute,
                 // so remove them from the headers.
@@ -57,13 +73,7 @@ impl LambdaResponse {
                     multi_value_headers: headers,
                 })
             }
-            RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse {
-                body,
-                status_code: status_code as i64,
-                is_base64_encoded: Some(is_base64_encoded),
-                headers: headers.clone(),
-                multi_value_headers: headers,
-            }),
+            #[cfg(feature = "alb")]
             RequestOrigin::Alb => LambdaResponse::Alb(AlbTargetGroupResponse {
                 body,
                 status_code: status_code as i64,
@@ -76,6 +86,7 @@ impl LambdaResponse {
                     parts.status.canonical_reason().unwrap_or_default()
                 )),
             }),
+            #[cfg(feature = "apigw_websockets")]
             RequestOrigin::WebSocket => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse {
                 body,
                 status_code: status_code as i64,


Test patch: