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

Issue:
Request path includes API gateway stage twice
**Background**

I have an API gateway hooked up to a lambda - I don't have any parameter mappings & I'm not using a custom domain.  The lambda uses `lambda_http` together with `axum`.  The API gateway has a stage called `Prod`, the invoke URL is along the lines of:

`https://blablabla.execute-api.ap-southeast-2.amazonaws.com/Prod`

**Current outcome**

When performing a request such as `POST https://blablabla.execute-api.ap-southeast-2.amazonaws.com/Prod/auth/login`,
`lambda_http` rewrites the request path to `/Prod/Prod/auth/login`.

**Expected outcome**

I would expect the request path to be `/Prod/auth/login`.  

**Other information**

While there is a workaround (rewrite the path in middleware), the underlying behavior of including the stage twice is very surprising to me - is is possible I've misconfigured something?

Related discussion in #107 and #450.







Discussion / hints:
Are you using APIGW Rest or HTTP? I wonder if APIGW started sending the stage in the path.

Can you print the content of `req.raw_http_path()`? That should not have the stage part. If it does, the event's data has changed.
HTTP.  `req.raw_http_path()` is `/Prod/auth/login`.

Fix patch:
diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs
index ea418595..bdb755ed 100644
--- a/lambda-http/src/request.rs
+++ b/lambda-http/src/request.rs
@@ -327,10 +327,21 @@ fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request<
 
 #[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))]
 fn apigw_path_with_stage(stage: &Option<String>, path: &str) -> String {
-    match stage {
-        None => path.into(),
-        Some(stage) if stage == "$default" => path.into(),
-        Some(stage) => format!("/{stage}{path}"),
+    if env::var("AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH").is_ok() {
+        return path.into();
+    }
+
+    let stage = match stage {
+        None => return path.into(),
+        Some(stage) if stage == "$default" => return path.into(),
+        Some(stage) => stage,
+    };
+
+    let prefix = format!("/{stage}/");
+    if path.starts_with(&prefix) {
+        path.into()
+    } else {
+        format!("/{stage}{path}")
     }
 }
 
@@ -531,7 +542,7 @@ mod tests {
         assert_eq!(req.method(), "GET");
         assert_eq!(
             req.uri(),
-            "https://wt6mne2s9k.execute-api.us-west-2.amazonaws.com/test/test/hello?name=me"
+            "https://wt6mne2s9k.execute-api.us-west-2.amazonaws.com/test/hello?name=me"
         );
 
         // Ensure this is an APIGW request
@@ -733,7 +744,7 @@ mod tests {
         );
         let req = result.expect("failed to parse request");
         assert_eq!(req.method(), "GET");
-        assert_eq!(req.uri(), "/test/test/hello?name=me");
+        assert_eq!(req.uri(), "/test/hello?name=me");
     }
 
     #[test]
@@ -768,4 +779,25 @@ mod tests {
         let url = build_request_uri("/path with spaces/and multiple segments", &HeaderMap::new(), None, None);
         assert_eq!("/path%20with%20spaces/and%20multiple%20segments", url);
     }
+
+    #[test]
+    fn deserializes_apigw_http_request_with_stage_in_path() {
+        let input = include_str!("../tests/data/apigw_v2_proxy_request_with_stage_in_path.json");
+        let result = from_str(input);
+        assert!(
+            result.is_ok(),
+            "event was not parsed as expected {result:?} given {input}"
+        );
+        let req = result.expect("failed to parse request");
+        assert_eq!("/Prod/my/path", req.uri().path());
+        assert_eq!("/Prod/my/path", req.raw_http_path());
+    }
+
+    #[test]
+    fn test_apigw_path_with_stage() {
+        assert_eq!("/path", apigw_path_with_stage(&None, "/path"));
+        assert_eq!("/path", apigw_path_with_stage(&Some("$default".into()), "/path"));
+        assert_eq!("/Prod/path", apigw_path_with_stage(&Some("Prod".into()), "/Prod/path"));
+        assert_eq!("/Prod/path", apigw_path_with_stage(&Some("Prod".into()), "/path"));
+    }
 }


Test patch:
diff --git a/lambda-http/tests/data/apigw_no_host.json b/lambda-http/tests/data/apigw_no_host.json
index 3143c81b..78a40dee 100644
--- a/lambda-http/tests/data/apigw_no_host.json
+++ b/lambda-http/tests/data/apigw_no_host.json
@@ -1,5 +1,5 @@
 {
-  "path": "/test/hello",
+  "path": "/hello",
   "headers": {
     "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
     "Accept-Encoding": "gzip, deflate, lzma, sdch, br",
diff --git a/lambda-http/tests/data/apigw_proxy_request.json b/lambda-http/tests/data/apigw_proxy_request.json
index 3b7cc9d2..61183846 100644
--- a/lambda-http/tests/data/apigw_proxy_request.json
+++ b/lambda-http/tests/data/apigw_proxy_request.json
@@ -1,5 +1,5 @@
 {
-  "path": "/test/hello",
+  "path": "/hello",
   "headers": {
     "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
     "Accept-Encoding": "gzip, deflate, lzma, sdch, br",
diff --git a/lambda-http/tests/data/apigw_v2_proxy_request_with_stage_in_path.json b/lambda-http/tests/data/apigw_v2_proxy_request_with_stage_in_path.json
new file mode 100644
index 00000000..86e173c6
--- /dev/null
+++ b/lambda-http/tests/data/apigw_v2_proxy_request_with_stage_in_path.json
@@ -0,0 +1,57 @@
+{
+  "version": "2.0",
+  "routeKey": "Prod",
+  "rawPath": "/Prod/my/path",
+  "rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
+  "cookies": [
+    "cookie1=value1",
+    "cookie2=value2"
+  ],
+  "headers": {
+    "Header1": "value1",
+    "Header2": "value2"
+  },
+  "queryStringParameters": {
+    "parameter1": "value1,value2",
+    "parameter2": "value"
+  },
+  "requestContext": {
+    "accountId": "123456789012",
+    "apiId": "api-id",
+    "authorizer": {
+      "jwt": {
+        "claims": {
+          "claim1": "value1",
+          "claim2": "value2"
+        },
+        "scopes": [
+          "scope1",
+          "scope2"
+        ]
+      }
+    },
+    "domainName": "id.execute-api.us-east-1.amazonaws.com",
+    "domainPrefix": "id",
+    "http": {
+      "method": "POST",
+      "path": "/Prod/my/path",
+      "protocol": "HTTP/1.1",
+      "sourceIp": "IP",
+      "userAgent": "agent"
+    },
+    "requestId": "id",
+    "routeKey": "Prod",
+    "stage": "Prod",
+    "time": "12/Mar/2020:19:03:58 +0000",
+    "timeEpoch": 1583348638390
+  },
+  "body": "Hello from Lambda",
+  "pathParameters": {
+    "parameter1": "value1"
+  },
+  "isBase64Encoded": false,
+  "stageVariables": {
+    "stageVariable1": "value1",
+    "stageVariable2": "value2"
+  }
+}
\ No newline at end of file