Repository: aws-powertools/powertools-lambda-python

Issue:
Case insensitive lookup of header values in http proxy events
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

Http headers are meant to be case insensitive, but lambda events use case sensitive dicts for the header keys.

For example getting the `Authorization` header you will need to do a case insensitive look up to support if the header is `authorization`. So you end up doing something like this.

```python3
authorization = None
for key, value in self.headers.items():
    if name.lower() == 'authorization'
        authorization = value
```

**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->

> Linked pull request : #185

Update `BaseProxyEvent.get_header_value` method to support case insensitive lookups by default

```python3
authorization = event.get_header_value("authorization")
```

```python3
    def get_header_value(
        self, name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False
    ) -> Optional[str]:
        """Get header value by name

        Parameters
        ----------
        name: str
            Header name
        default_value: str, optional
            Default value if no value was found by name
        case_sensitive: bool
            Whether to use a case sensitive look up
        Returns
        -------
        str, optional
            Header value
        """
        if case_sensitive:
            return self.headers.get(name, default_value)
        
        return next((value for key, value in self.headers.items() if name.lower() == key.lower()), default_value)
```

**Questions**

* Should we default to case insensitive look ups? This is what most people would want?
* Just return the first match? Or support returning all matches?

**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
Use the requests CaseInsensitiveDict or port it :
* https://github.com/psf/requests/blob/333fa8d3f201f68b82d9e8f26be4987e869fd3bf/requests/structures.py#L15

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
From the Http 1.1 spec, https headers are not supposed to be case sensitive:  https://www.w3.org/Protocols/rfc2616/rfc2616.html


Discussion:

Resolving patch:
diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py
--- a/aws_lambda_powertools/utilities/data_classes/common.py
+++ b/aws_lambda_powertools/utilities/data_classes/common.py
@@ -48,7 +48,9 @@ def get_query_string_value(self, name: str, default_value: Optional[str] = None)
         params = self.query_string_parameters
         return default_value if params is None else params.get(name, default_value)
 
-    def get_header_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]:
+    def get_header_value(
+        self, name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False
+    ) -> Optional[str]:
         """Get header value by name
 
         Parameters
@@ -57,9 +59,14 @@ def get_header_value(self, name: str, default_value: Optional[str] = None) -> Op
             Header name
         default_value: str, optional
             Default value if no value was found by name
+        case_sensitive: bool
+            Whether to use a case sensitive look up
         Returns
         -------
         str, optional
             Header value
         """
-        return self.headers.get(name, default_value)
+        if case_sensitive:
+            return self.headers.get(name, default_value)
+
+        return next((value for key, value in self.headers.items() if name.lower() == key.lower()), default_value)


Test patch:
diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py
--- a/tests/functional/test_lambda_trigger_events.py
+++ b/tests/functional/test_lambda_trigger_events.py
@@ -670,6 +670,10 @@ def test_base_proxy_event_get_header_value():
     value = event.get_header_value("test", default_value)
     assert value == set_value
 
+    # Verify that the default look is case insensitive
+    value = event.get_header_value("Test")
+    assert value == set_value
+
     value = event.get_header_value("unknown", default_value)
     assert value == default_value
 
@@ -677,6 +681,29 @@ def test_base_proxy_event_get_header_value():
     assert value is None
 
 
+def test_base_proxy_event_get_header_value_case_insensitive():
+    default_value = "default"
+    set_value = "value"
+
+    event = BaseProxyEvent({"headers": {}})
+
+    event._data["headers"] = {"Test": set_value}
+    value = event.get_header_value("test", case_sensitive=True)
+    assert value is None
+
+    value = event.get_header_value("test", default_value=default_value, case_sensitive=True)
+    assert value == default_value
+
+    value = event.get_header_value("Test", case_sensitive=True)
+    assert value == set_value
+
+    value = event.get_header_value("unknown", default_value, case_sensitive=True)
+    assert value == default_value
+
+    value = event.get_header_value("unknown", case_sensitive=True)
+    assert value is None
+
+
 def test_kinesis_stream_event():
     event = KinesisStreamEvent(load_event("kinesisStreamEvent.json"))