diff --git a/bottlecap/src/appsec/processor/response.rs b/bottlecap/src/appsec/processor/response.rs index b07fa5ea7..996bac90a 100644 --- a/bottlecap/src/appsec/processor/response.rs +++ b/bottlecap/src/appsec/processor/response.rs @@ -94,6 +94,9 @@ impl InvocationPayload for RawPayload { HashMap::default() } fn response_body<'a>(&'a self) -> Option> { + if self.data.is_empty() { + return None; + } Some(Box::new(Cursor::new(&self.data))) } } @@ -132,4 +135,28 @@ mod test { assert!(response.response_headers_no_cookies().is_empty()); assert_eq!(response.response_status_code(), Some(0)); } + + #[test] + fn test_empty_body_in_apigw_response() { + let response = r#"{ + "statusCode": 204, + "headers": {}, + "multiValueHeaders": {}, + "body": "" + }"#; + 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()); + } + + #[test] + fn test_empty_raw_response() { + let response = ExpectedResponseFormat::Raw + .parse(b"") + .expect("response should have parsed cleanly") + .expect("response should have been Some"); + assert!(response.response_body().is_none()); + } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/body.rs b/bottlecap/src/lifecycle/invocation/triggers/body.rs index 6d09961fd..1c3acb676 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/body.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/body.rs @@ -16,11 +16,19 @@ pub struct Body { impl Body { /// Obtains a reader to the data contained in this [`Body`], decoded from /// Base64 if [`Body::is_base64_encoded`] is `true`. + /// + /// Returns [`None`] if there is no body, including when it is an empty + /// string, which is how many event sources represent the absence of a + /// body (e.g, `GET` requests, `204` responses, etc...). pub(crate) fn reader<'a>(&'a self) -> Result>, base64::DecodeError> { let Some(body) = &self.body else { return Ok(None); }; + if body.is_empty() { + return Ok(None); + } + if self.is_base64_encoded { let body = base64::engine::general_purpose::STANDARD.decode(body)?; let reader = Bytes::from(body).reader(); @@ -30,3 +38,44 @@ impl Body { } } } + +#[cfg(test)] +mod tests { + use std::io::read_to_string; + + use super::*; + + #[test] + fn test_reader_no_body() { + let body = Body { + body: None, + is_base64_encoded: false, + }; + assert!(body.reader().expect("should not fail").is_none()); + } + + #[test] + fn test_reader_empty_body() { + let body = Body { + body: Some(String::new()), + is_base64_encoded: false, + }; + assert!(body.reader().expect("should not fail").is_none()); + } + + #[test] + fn test_reader_base64_body() { + let body = Body { + body: Some("eyJmb28iOiJiYXIifQ==".to_string()), + is_base64_encoded: true, + }; + let reader = body + .reader() + .expect("should not fail") + .expect("should be Some"); + assert_eq!( + read_to_string(reader).expect("should read cleanly"), + r#"{"foo":"bar"}"# + ); + } +}