Skip to content

Commit f26b481

Browse files
authored
Merge pull request #159 from Flagsmith/feat/environment-document-304
feat: Set If-Modified-Since header for environment document requests
2 parents 96f76aa + c1e26fc commit f26b481

File tree

3 files changed

+72
-6
lines changed

3 files changed

+72
-6
lines changed

src/edge_proxy/environments.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import typing
22
from datetime import datetime
3+
from email.utils import formatdate
34
from functools import lru_cache
45

56
import httpx
7+
import starlette.status
68
import structlog
79
from flag_engine.engine import (
810
get_environment_feature_state,
@@ -22,7 +24,7 @@
2224
map_traits_to_response_data,
2325
)
2426
from edge_proxy.models import IdentityWithTraits
25-
from edge_proxy.settings import AppSettings
27+
from edge_proxy.settings import AppSettings, EnvironmentKeyPair
2628

2729
logger = structlog.get_logger(__name__)
2830

@@ -58,9 +60,7 @@ async def refresh_environment_caches(self):
5860
received_error = False
5961
for key_pair in self.settings.environment_key_pairs:
6062
try:
61-
environment_document = await self._fetch_document(
62-
key_pair.server_side_key
63-
)
63+
environment_document = await self._fetch_document(key_pair)
6464
if self.cache.put_environment(
6565
environment_api_key=key_pair.client_side_key,
6666
environment_document=environment_document,
@@ -142,11 +142,41 @@ def get_environment(self, client_side_key: str) -> dict[str, typing.Any]:
142142
return environment_document
143143
raise FlagsmithUnknownKeyError(client_side_key)
144144

145-
async def _fetch_document(self, server_side_key: str) -> dict[str, typing.Any]:
145+
async def _fetch_document(
146+
self, key_pair: EnvironmentKeyPair
147+
) -> dict[str, typing.Any]:
148+
headers = {
149+
"X-Environment-Key": key_pair.server_side_key,
150+
}
151+
environment_document = self.cache.get_environment(
152+
environment_api_key=key_pair.client_side_key
153+
)
154+
if environment_document:
155+
updated_at: str = environment_document.get("updated_at")
156+
if updated_at:
157+
try:
158+
epoch_seconds = datetime.fromisoformat(updated_at).timestamp()
159+
# Same implementation as https://docs.djangoproject.com/en/4.2/ref/utils/#django.utils.http.http_date
160+
headers["If-Modified-Since"] = formatdate(
161+
epoch_seconds, usegmt=True
162+
)
163+
except ValueError:
164+
logger.warning(
165+
f"failed to parse updated_at, environment={key_pair.client_side_key} updated_at={updated_at}"
166+
)
167+
else:
168+
logger.warning(
169+
f"received environment with no updated_at: {key_pair.client_side_key}"
170+
)
146171
response = await self._client.get(
147172
url=f"{self.settings.api_url}/environment-document/",
148-
headers={"X-Environment-Key": server_side_key},
173+
headers=headers,
149174
)
175+
if response.status_code == starlette.status.HTTP_304_NOT_MODIFIED:
176+
assert environment_document, (
177+
f"GET /environment-document returned 304 without a cached document. environment={key_pair.client_side_key}"
178+
)
179+
return environment_document
150180
response.raise_for_status()
151181
return orjson.loads(response.text)
152182

tests/fixtures/response_data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191

9292

9393
environment_1 = {
94+
"updated_at": "1969-07-20T20:17:40Z",
9495
"feature_states": [
9596
_environment_feature_state_1,
9697
_environment_feature_state_2,

tests/test_environments.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,41 @@ async def test_refresh_environment_caches_clears_endpoint_caches_if_environment_
196196
assert environment_service.get_flags_response_data.cache_info().currsize == 0
197197

198198

199+
@pytest.mark.asyncio
200+
async def test_refresh_environment_caches_sets_last_modified_if_environment_was_cached(
201+
mocker: MockerFixture,
202+
):
203+
# Given
204+
# An EnvironmentService that saves the last If-Modified-Since request header its client used
205+
if_modified_since = ""
206+
207+
def save_if_modified_since(**kwargs):
208+
nonlocal if_modified_since
209+
if headers := kwargs.get("headers"):
210+
if_modified_since = headers.get("If-Modified-Since")
211+
return mocker.MagicMock(
212+
text=orjson.dumps(environment_1),
213+
)
214+
215+
mocked_client = mocker.AsyncMock()
216+
mocked_client.get.side_effect = save_if_modified_since
217+
environment_service = EnvironmentService(settings=settings, client=mocked_client)
218+
219+
# When
220+
# We refresh its environment caches
221+
await environment_service.refresh_environment_caches()
222+
# Then
223+
# No If-Modified-Since request header is sent initially
224+
assert not if_modified_since
225+
226+
# When
227+
# We refresh the caches while having an existing valid cache
228+
await environment_service.refresh_environment_caches()
229+
# Then
230+
# If-Modified-Since is set to the environment's updated_at in HTTP date format
231+
assert if_modified_since == "Sun, 20 Jul 1969 20:17:40 GMT"
232+
233+
199234
@pytest.mark.asyncio
200235
async def test_get_identity_flags_response_skips_cache_for_different_identity(
201236
mocker: MockerFixture,

0 commit comments

Comments
 (0)