Skip to content

Commit ccfbc94

Browse files
fix(appsync): make contextual data accessible for async functions (#5317)
* Making contextual data accessible for async functions * V4 comment * Reverting test
1 parent c11d25c commit ccfbc94

File tree

4 files changed

+87
-3
lines changed

4 files changed

+87
-3
lines changed

aws_lambda_powertools/event_handler/appsync.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,13 @@ def lambda_handler(event, context):
149149
Router.current_event = data_model(event)
150150
response = self._call_single_resolver(event=event, data_model=data_model)
151151

152-
self.clear_context()
152+
# We don't clear the context for coroutines because we don't have control over the event loop.
153+
# If we clean the context immediately, it might not be available when the coroutine is actually executed.
154+
# For single async operations, the context should be cleaned up manually after the coroutine completes.
155+
# See: https://github.com/aws-powertools/powertools-lambda-python/issues/5290
156+
# REVIEW: Review this support in Powertools V4
157+
if not asyncio.iscoroutine(response):
158+
self.clear_context()
153159

154160
return response
155161

docs/core/event_handler/appsync.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ Let's assume you have `split_operation.py` as your Lambda function entrypoint an
270270

271271
You can use `append_context` when you want to share data between your App and Router instances. Any data you share will be available via the `context` dictionary available in your App or Router context.
272272

273-
???+ info
274-
For safety, we always clear any data available in the `context` dictionary after each invocation.
273+
???+ warning
274+
For safety, we clear the context after each invocation, except for async single resolvers. For these, use `app.context.clear()` before returning the function.
275275

276276
???+ tip
277277
This can also be useful for middlewares injecting contextual information before a request is processed.

tests/functional/event_handler/required_dependencies/appsync/test_appsync_batch_resolvers.py

+38
Original file line numberDiff line numberDiff line change
@@ -943,3 +943,41 @@ def get_user(event: List) -> List:
943943

944944
# THEN the resolver must be able to return a field in the batch_current_event
945945
assert ret[0] == mock_event[0]["identity"]["sub"]
946+
947+
948+
def test_context_is_accessible_in_sync_batch_resolver():
949+
mock_event = load_event("appSyncBatchEvent.json")
950+
951+
# GIVEN An instance of AppSyncResolver and a resolver function registered with the app
952+
app = AppSyncResolver()
953+
954+
@app.batch_resolver(field_name="createSomething")
955+
def get_user(event: List) -> List:
956+
return [app.context.get("project_name")]
957+
958+
# WHEN we resolve the event
959+
app.append_context(project_name="powertools")
960+
ret = app.resolve(mock_event, {})
961+
962+
# THEN the resolver must be able to return a field in the batch_current_event
963+
assert app.context == {}
964+
assert ret[0] == "powertools"
965+
966+
967+
def test_context_is_accessible_in_async_batch_resolver():
968+
mock_event = load_event("appSyncBatchEvent.json")
969+
970+
# GIVEN An instance of AppSyncResolver and a resolver function registered with the app
971+
app = AppSyncResolver()
972+
973+
@app.async_batch_resolver(field_name="createSomething")
974+
async def get_user(event: List) -> List:
975+
return [app.context.get("project_name")]
976+
977+
# WHEN we resolve the event
978+
app.append_context(project_name="powertools")
979+
ret = app.resolve(mock_event, {})
980+
981+
# THEN the resolver must be able to return a field in the batch_current_event
982+
assert app.context == {}
983+
assert ret[0] == "powertools"

tests/functional/event_handler/required_dependencies/appsync/test_appsync_single_resolvers.py

+40
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,43 @@ def get_user(id: str) -> dict: # noqa AA03 VNE003
289289

290290
# THEN the resolver must be able to return a field in the current_event
291291
assert ret == mock_event["identity"]["sub"]
292+
293+
294+
def test_route_context_is_not_cleared_after_resolve_async():
295+
# GIVEN
296+
app = AppSyncResolver()
297+
event = {"typeName": "Query", "fieldName": "listLocations", "arguments": {"name": "value"}}
298+
299+
@app.resolver(field_name="listLocations")
300+
async def get_locations(name: str):
301+
return f"get_locations#{name}"
302+
303+
# WHEN event resolution kicks in
304+
app.append_context(is_admin=True)
305+
app.resolve(event, {})
306+
307+
# THEN context should be empty
308+
assert app.context == {"is_admin": True}
309+
310+
311+
def test_route_context_is_manually_cleared_after_resolve_async():
312+
# GIVEN
313+
# GIVEN
314+
app = AppSyncResolver()
315+
316+
mock_event = {"typeName": "Customer", "fieldName": "field", "arguments": {}}
317+
318+
@app.resolver(field_name="field")
319+
async def get_async():
320+
app.context.clear()
321+
await asyncio.sleep(0.0001)
322+
return "value"
323+
324+
# WHEN
325+
mock_context = LambdaContext()
326+
app.append_context(is_admin=True)
327+
result = app.resolve(mock_event, mock_context)
328+
329+
# THEN
330+
assert asyncio.run(result) == "value"
331+
assert app.context == {}

0 commit comments

Comments
 (0)