Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,12 +785,23 @@ def _add_cors(self, event: ResponseEventT, cors: CORSConfig):
if origin is not None:
cors_headers = cors.to_dict(origin)
# Keep any Vary value the route already set, and add Origin to it
existing_vary = self.response.headers.get("Vary")
if isinstance(existing_vary, list):
existing_vary = ", ".join(existing_vary)
if existing_vary and "Vary" in cors_headers:
cors_headers["Vary"] = f"{existing_vary}, Origin"
vary = cors_headers.pop("Vary", None)
self.response.headers.update(cors_headers)
if vary:
self._add_vary(vary)

def _add_vary(self, field: str):
"""Add a request header name to the Vary response header, keeping any value already set"""
existing_vary = self.response.headers.get("Vary")
if isinstance(existing_vary, list):
existing_vary = ", ".join(existing_vary)
if not existing_vary:
self.response.headers["Vary"] = field
return
values = {value.strip().lower() for value in existing_vary.split(",")}
# "*" already covers every request header
if field.lower() not in values and "*" not in values:
self.response.headers["Vary"] = f"{existing_vary}, {field}"

def _add_cache_control(self, cache_control: str):
"""Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used."""
Expand Down Expand Up @@ -857,6 +868,10 @@ def _route(self, event: ResponseEventT, cors: CORSConfig | None):
self._add_cors(event, cors or CORSConfig())
if self.route.cache_control:
self._add_cache_control(self.route.cache_control)
compression = self.response.compress if self.response.compress is not None else self.route.compress
if compression:
# The body depends on Accept-Encoding, so shared caches must not reuse it across encodings
self._add_vary("Accept-Encoding")
if self._has_compression_enabled(
route_compression=self.route.compress,
response_compression=self.response.compress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,48 @@ def return_text() -> Response:
assert result["body"] == expected_value


@pytest.mark.parametrize(
"route_compress, response_compress, response_headers, accept_encoding, expected_vary",
[
pytest.param(True, None, None, "gzip", ["Accept-Encoding"], id="compressed response"),
pytest.param(True, None, None, "deflate", ["Accept-Encoding"], id="uncompressed response of a compress route"),
pytest.param(False, True, None, "gzip", ["Accept-Encoding"], id="compress enabled on the response"),
pytest.param(True, None, {"Vary": "Origin"}, "gzip", ["Origin, Accept-Encoding"], id="existing Vary kept"),
pytest.param(True, None, {"Vary": "accept-encoding"}, "gzip", ["accept-encoding"], id="not added twice"),
pytest.param(True, None, {"Vary": "*"}, "gzip", ["*"], id="wildcard Vary kept as is"),
pytest.param(
True,
None,
{"Vary": ["Accept", "Accept-Language"]},
"gzip",
["Accept, Accept-Language, Accept-Encoding"],
id="existing Vary list kept",
),
pytest.param(False, None, None, "gzip", None, id="compression not enabled"),
pytest.param(True, False, None, "gzip", None, id="compression disabled on the response"),
],
)
def test_compress_sets_vary_accept_encoding(
route_compress: bool,
response_compress: bool | None,
response_headers: dict | None,
accept_encoding: str,
expected_vary: list[str] | None,
):
# GIVEN a route whose body depends on the request Accept-Encoding when compression is enabled
app = ApiGatewayResolver()

@app.get("/my/path", compress=route_compress)
def return_text() -> Response:
return Response(200, content_types.TEXT_PLAIN, "Foo", headers=response_headers, compress=response_compress)

# WHEN calling the event handler
result = app({"path": "/my/path", "httpMethod": "GET", "headers": {"Accept-Encoding": accept_encoding}}, None)

# THEN Vary names Accept-Encoding whenever compression can change the body, so caches keep encodings apart
assert result["multiValueHeaders"].get("Vary") == expected_vary


@pytest.mark.parametrize(
"accept_encoding, expected_compressed",
[
Expand Down