diff --git a/bottlecap/src/appsec/processor/response.rs b/bottlecap/src/appsec/processor/response.rs index d3b8d15d0..17194b86d 100644 --- a/bottlecap/src/appsec/processor/response.rs +++ b/bottlecap/src/appsec/processor/response.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; use std::io::Cursor; -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; use crate::appsec::processor::InvocationPayload; -use crate::lifecycle::invocation::triggers::{body::Body, lowercase_key}; +use crate::lifecycle::invocation::triggers::body::Body; /// The expected payload of a response. This is different from trigger to trigger. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -50,9 +50,9 @@ impl Default for ExpectedResponseFormat { #[serde(rename_all = "camelCase")] struct ApiGatewayResponse { status_code: i64, - #[serde(deserialize_with = "lowercase_key", default)] + #[serde(deserialize_with = "nullable_lowercase_key", default)] headers: HashMap, - #[serde(deserialize_with = "lowercase_key", default)] + #[serde(deserialize_with = "nullable_lowercase_key", default)] multi_value_headers: HashMap>, #[serde(flatten)] body: Body, @@ -101,3 +101,39 @@ impl InvocationPayload for RawPayload { Some(Box::new(Cursor::new(&self.data))) } } + +fn nullable_lowercase_key<'de, D, V>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + V: Deserialize<'de>, +{ + let Some(map) = Option::>::deserialize(deserializer)? else { + return Ok(HashMap::default()); + }; + Ok(map + .into_iter() + .map(|(key, value)| (key.to_lowercase(), value)) + .collect()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_null_fields_in_apigw_response() { + let response = r#"{ + "statusCode": 0, + "headers": null, + "multiValueHeaders": null, + "body": null + }"#; + let response = ExpectedResponseFormat::ApiGatewayResponse + .parse(response.as_bytes()) + .expect("response should have parsed cleanly") + .expect("response should have been Some"); + assert!(response.response_body().is_none()); + assert!(response.response_headers_no_cookies().is_empty()); + assert_eq!(response.response_status_code(), Some(0)); + } +}