Repository: cargo-lambda/cargo-lambda (language: python)

Issue:
watch subcommand ignores http status codes on v0.21.0
I am hitting an issue with `cargo lambda watch` in an axum lambda project where any returned errors are being defaulted to status code `200`.

Minimum repro:
```rust
use axum::{routing::get, Router};
use http::StatusCode;
use lambda_http::{run, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let app = Router::new().route("/", get(test));
    run(app).await
}

async fn test() -> Result<String, (StatusCode, String)> {
    Err((StatusCode::INTERNAL_SERVER_ERROR, "Expected Error".to_string()))
}
```

```toml
axum = "0.6.18"
http = "0.2.9"
lambda_http = "0.8.1"
lambda_runtime = "0.8.2"
```

With the above example, I would expect hitting the `localhost:9000/` endpoint to return status code `500` however it returns `200`. I have tested rolling back to cargo-lambda v0.20.4 and can confirm it correctly returns status codes, so I believe this problem to be somewhere between the two releases.

Please let me know if there is any further info I can provide to helping track this one down or if there's anything you'd like me to test.


Discussion / hints:

Fix patch:
diff --git a/crates/cargo-lambda-build/src/lib.rs b/crates/cargo-lambda-build/src/lib.rs
index a342412c..0bacbed9 100644
--- a/crates/cargo-lambda-build/src/lib.rs
+++ b/crates/cargo-lambda-build/src/lib.rs
@@ -451,7 +451,7 @@ mod test {
         let path = Path::new("");
         assert_eq!(
             "",
-            convert_to_unix_path(&path).expect("failed to convert file path")
+            convert_to_unix_path(path).expect("failed to convert file path")
         );
     }
 }
diff --git a/crates/cargo-lambda-watch/src/error.rs b/crates/cargo-lambda-watch/src/error.rs
index 09a884cc..16de37bc 100644
--- a/crates/cargo-lambda-watch/src/error.rs
+++ b/crates/cargo-lambda-watch/src/error.rs
@@ -85,4 +85,8 @@ pub enum ServerError {
     #[error("the streaming prelude is missing from the Lambda response")]
     #[diagnostic()]
     MissingStreamingPrelude,
+
+    #[error(transparent)]
+    #[diagnostic()]
+    InvalidStatusCode(#[from] hyper::http::status::InvalidStatusCode),
 }
diff --git a/crates/cargo-lambda-watch/src/trigger_router.rs b/crates/cargo-lambda-watch/src/trigger_router.rs
index a21d9edc..937bd4e5 100644
--- a/crates/cargo-lambda-watch/src/trigger_router.rs
+++ b/crates/cargo-lambda-watch/src/trigger_router.rs
@@ -157,45 +157,21 @@ async fn furls_handler(
 
     let mut builder = Response::builder().status(status_code);
 
-    if is_streaming_response(&info.headers) && status_code == StatusCode::OK {
-        let status = create_streaming_response(&mut builder, &mut body).await?;
+    let response = if status_code == StatusCode::OK {
+        if is_streaming_response(&info.headers) {
+            let status = create_streaming_response(&mut builder, &mut body).await?;
 
-        builder
-            .status(status)
-            .body(body)
-            .map_err(ServerError::ResponseBuild)
-    } else {
-        let body = to_bytes(body)
-            .await
-            .map_err(ServerError::DataDeserialization)?;
-        let resp_event: ApiGatewayV2httpResponse =
-            serde_json::from_slice(&body).map_err(ServerError::SerializationError)?;
-
-        let is_base64_encoded = resp_event.is_base64_encoded.unwrap_or(false);
-        let resp_body = match resp_event.body.unwrap_or(LambdaBody::Empty) {
-            LambdaBody::Empty => Body::empty(),
-            b if is_base64_encoded => Body::from(
-                b64::STANDARD
-                    .decode(b.as_ref())
-                    .map_err(ServerError::BodyDecodeError)?,
-            ),
-            LambdaBody::Text(s) => Body::from(s),
-            LambdaBody::Binary(b) => Body::from(b),
-        };
-        if let Some(headers) = builder.headers_mut() {
-            headers.extend(resp_event.headers);
-            headers.extend(resp_event.multi_value_headers);
-
-            resp_event.cookies.iter().try_for_each(|cookie| {
-                let header_value = HeaderValue::try_from(cookie)
-                    .map_err(|e| ServerError::ResponseBuild(e.into()))?;
-                headers.append(header::SET_COOKIE, header_value);
-                Ok::<(), ServerError>(())
-            })?;
+            builder.status(status).body(body)
+        } else {
+            let (status, body) = create_buffered_response(&mut builder, &mut body).await?;
+
+            builder.status(status).body(body)
         }
+    } else {
+        builder.body(body)
+    };
 
-        builder.body(resp_body).map_err(ServerError::ResponseBuild)
-    }
+    response.map_err(ServerError::ResponseBuild)
 }
 
 async fn invoke_handler(
@@ -344,6 +320,45 @@ fn is_streaming_response(headers: &HeaderMap) -> bool {
         .unwrap_or_default()
 }
 
+async fn create_buffered_response(
+    builder: &mut Builder,
+    body: &mut Body,
+) -> Result<(StatusCode, Body), ServerError> {
+    let body = to_bytes(body)
+        .await
+        .map_err(ServerError::DataDeserialization)?;
+    let resp_event: ApiGatewayV2httpResponse =
+        serde_json::from_slice(&body).map_err(ServerError::SerializationError)?;
+
+    let is_base64_encoded = resp_event.is_base64_encoded.unwrap_or(false);
+    let resp_body = match resp_event.body.unwrap_or(LambdaBody::Empty) {
+        LambdaBody::Empty => Body::empty(),
+        b if is_base64_encoded => Body::from(
+            b64::STANDARD
+                .decode(b.as_ref())
+                .map_err(ServerError::BodyDecodeError)?,
+        ),
+        LambdaBody::Text(s) => Body::from(s),
+        LambdaBody::Binary(b) => Body::from(b),
+    };
+    if let Some(headers) = builder.headers_mut() {
+        headers.extend(resp_event.headers);
+        headers.extend(resp_event.multi_value_headers);
+
+        resp_event.cookies.iter().try_for_each(|cookie| {
+            let header_value =
+                HeaderValue::try_from(cookie).map_err(|e| ServerError::ResponseBuild(e.into()))?;
+            headers.append(header::SET_COOKIE, header_value);
+            Ok::<(), ServerError>(())
+        })?;
+    }
+
+    let status: StatusCode = StatusCode::from_u16(resp_event.status_code as u16)
+        .map_err(ServerError::InvalidStatusCode)?;
+
+    Ok((status, resp_body))
+}
+
 #[cfg(test)]
 mod test {
     use super::extract_path_parameters;


Test patch:
diff --git a/crates/cargo-lambda-cli/tests/cargo_lambda.rs b/crates/cargo-lambda-cli/tests/cargo_lambda.rs
index 7f15602d..6a4616b1 100644
--- a/crates/cargo-lambda-cli/tests/cargo_lambda.rs
+++ b/crates/cargo-lambda-cli/tests/cargo_lambda.rs
@@ -37,7 +37,7 @@ fn test_build_basic_zip_function() {
 
     let project = test_project(root);
     cargo_lambda_build(project.root())
-        .args(&["--output-format", "zip"])
+        .args(["--output-format", "zip"])
         .assert()
         .success();
 
@@ -51,10 +51,7 @@ fn test_build_basic_zip_function() {
     assert!(
         zip.by_name("bootstrap").is_ok(),
         "bootstrap is not in the zip archive. Files in zip: {:?}",
-        zip.file_names()
-            .into_iter()
-            .collect::<Vec<&str>>()
-            .join(", ")
+        zip.file_names().collect::<Vec<&str>>().join(", ")
     );
 }
 
@@ -224,7 +221,7 @@ fn test_build_basic_zip_extension() {
     let project = test_project(root);
     cargo_lambda_build(project.root())
         .arg("--extension")
-        .args(&["--output-format", "zip"])
+        .args(["--output-format", "zip"])
         .assert()
         .success();
 
@@ -235,10 +232,7 @@ fn test_build_basic_zip_extension() {
     assert!(
         zip.by_name("extensions/test-basic-extension").is_ok(),
         "test-basic-extension is not in the zip archive. Files in zip: {:?}",
-        zip.file_names()
-            .into_iter()
-            .collect::<Vec<&str>>()
-            .join(", ")
+        zip.file_names().collect::<Vec<&str>>().join(", ")
     );
 }
 
@@ -255,7 +249,7 @@ fn test_build_internal_zip_extension() {
     cargo_lambda_build(project.root())
         .arg("--extension")
         .arg("--internal")
-        .args(&["--output-format", "zip"])
+        .args(["--output-format", "zip"])
         .assert()
         .success();
 
@@ -266,9 +260,6 @@ fn test_build_internal_zip_extension() {
     assert!(
         zip.by_name("test-internal-extension").is_ok(),
         "test-internal-extension is not in the zip archive. Files in zip: {:?}",
-        zip.file_names()
-            .into_iter()
-            .collect::<Vec<&str>>()
-            .join(", ")
+        zip.file_names().collect::<Vec<&str>>().join(", ")
     );
 }