From 33baa2203d5a2becbc3a0e39f9ebba547d7af497 Mon Sep 17 00:00:00 2001 From: hirenkumar-n-dholariya Date: Thu, 16 Apr 2026 17:06:59 -0400 Subject: [PATCH 1/8] feat(event-handler): add _registered_api_adapter_async() internal building block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add async counterpart to _registered_api_adapter() - Detects if route handler result is a coroutine using inspect.iscoroutine() - Awaits async handlers; passes sync handlers through unchanged - _to_response() remains sync (CPU-bound, no async benefit) - Move import inspect to top-level imports (consistent with file style) - Nothing calls this in the resolve chain yet — internal building block only Closes #8135 Part of parent: #3934 Next step: #8137 (public resolve_async()) will wire this in Signed-off-by: hirenkumar-n-dholariya --- .../event_handler/api_gateway.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 041f6f7abf3..11a28096909 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -3,6 +3,7 @@ import base64 import json import logging +import inspect import re import traceback import warnings @@ -1474,7 +1475,64 @@ def _registered_api_adapter( return app._to_response(next_middleware(**route_args)) +async def _registered_api_adapter_async( + app: ApiGatewayResolver, + next_middleware: Callable[..., Any], +) -> dict | tuple | Response | BedrockResponse: + """ + Async version of _registered_api_adapter. + + Detects if the route handler is a coroutine and awaits it. + _to_response() stays sync (CPU-bound — no async benefit). + + **IMPORTANT: This is an internal building block only. + Nothing calls it in the resolve chain yet. It will be used + by resolve_async() (see issue #8137). + + Parameters + ---------- + app: ApiGatewayResolver + The API Gateway resolver + next_middleware: Callable[..., Any] + The function to handle the API + + Returns + ------- + Response + The API Response Object + """ + route_args: dict = app.context.get("_route_args", {}) + logger.debug(f"Calling async API Route Handler: {route_args}") + + # Inject a Request object when the handler declares a parameter typed as Request. + # Lookup is cached on the Route object to avoid repeated signature inspection. + route: Route | None = app.context.get("_route") + if route is not None: + if not route.request_param_name_checked: + route.request_param_name = _find_request_param_name(next_middleware) + route.request_param_name_checked = True + if route.request_param_name: + route_args = {**route_args, route.request_param_name: app.request} + + # Resolve Depends() parameters (same as sync version) + if route.has_dependencies: + from aws_lambda_powertools.event_handler.depends import build_dependency_tree, solve_dependencies + + dep_values = solve_dependencies( + dependant=build_dependency_tree(route.func), + request=app.request, + dependency_overrides=app.dependency_overrides or None, + ) + route_args.update(dep_values) + + # Call handler — detect if result is a coroutine and await it + result = next_middleware(**route_args) + if inspect.iscoroutine(result): + result = await result + # _to_response is CPU-bound, stays sync + return app._to_response(result) + class ApiGatewayResolver(BaseRouter): """API Gateway, VPC Lattice, Bedrock and ALB proxy resolver From cf55e9356a817ef0fa79613a0eddf2076af99aca Mon Sep 17 00:00:00 2001 From: hirenkumar-n-dholariya Date: Fri, 17 Apr 2026 07:16:56 -0400 Subject: [PATCH 2/8] refactor(event-handler): move _registered_api_adapter_async to async_utils.py Per maintainer feedback on #8157 - function belongs in async_utils.py alongside other async event handler internals (wrap_middleware_async, _run_sync_middleware_in_thread, etc.) Signed-off-by: hirenkumar-n-dholariya --- .../event_handler/api_gateway.py | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 11a28096909..041f6f7abf3 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -3,7 +3,6 @@ import base64 import json import logging -import inspect import re import traceback import warnings @@ -1475,64 +1474,7 @@ def _registered_api_adapter( return app._to_response(next_middleware(**route_args)) -async def _registered_api_adapter_async( - app: ApiGatewayResolver, - next_middleware: Callable[..., Any], -) -> dict | tuple | Response | BedrockResponse: - """ - Async version of _registered_api_adapter. - - Detects if the route handler is a coroutine and awaits it. - _to_response() stays sync (CPU-bound — no async benefit). - - **IMPORTANT: This is an internal building block only. - Nothing calls it in the resolve chain yet. It will be used - by resolve_async() (see issue #8137). - - Parameters - ---------- - app: ApiGatewayResolver - The API Gateway resolver - next_middleware: Callable[..., Any] - The function to handle the API - - Returns - ------- - Response - The API Response Object - """ - route_args: dict = app.context.get("_route_args", {}) - logger.debug(f"Calling async API Route Handler: {route_args}") - - # Inject a Request object when the handler declares a parameter typed as Request. - # Lookup is cached on the Route object to avoid repeated signature inspection. - route: Route | None = app.context.get("_route") - if route is not None: - if not route.request_param_name_checked: - route.request_param_name = _find_request_param_name(next_middleware) - route.request_param_name_checked = True - if route.request_param_name: - route_args = {**route_args, route.request_param_name: app.request} - - # Resolve Depends() parameters (same as sync version) - if route.has_dependencies: - from aws_lambda_powertools.event_handler.depends import build_dependency_tree, solve_dependencies - - dep_values = solve_dependencies( - dependant=build_dependency_tree(route.func), - request=app.request, - dependency_overrides=app.dependency_overrides or None, - ) - route_args.update(dep_values) - - # Call handler — detect if result is a coroutine and await it - result = next_middleware(**route_args) - if inspect.iscoroutine(result): - result = await result - # _to_response is CPU-bound, stays sync - return app._to_response(result) - class ApiGatewayResolver(BaseRouter): """API Gateway, VPC Lattice, Bedrock and ALB proxy resolver From 2612941ed5d0b8677897686253440cf0528ab075 Mon Sep 17 00:00:00 2001 From: hirenkumar-n-dholariya Date: Fri, 17 Apr 2026 07:18:03 -0400 Subject: [PATCH 3/8] test(event-handler): add unit tests for _registered_api_adapter_async() Tests cover sync handler, async handler, and mixed scenarios as required by issue #8135 Signed-off-by: hirenkumar-n-dholariya --- .../test_registered_api_adapter_async.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py diff --git a/aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py b/aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py new file mode 100644 index 00000000000..c1b02e9731a --- /dev/null +++ b/aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py @@ -0,0 +1,72 @@ +""" +Unit tests for _registered_api_adapter_async() +Covers: sync handler, async handler, and mixed scenarios +""" +import asyncio +import inspect +import pytest +from unittest.mock import MagicMock + + +# ── helpers ────────────────────────────────────────────────────────────────── + +def _make_app(route_args=None, route=None): + """Build a minimal mock app context.""" + app = MagicMock() + app.context = {"_route_args": route_args or {}, "_route": route} + app.request = MagicMock() + app._to_response = lambda result: result # pass-through for testing + return app + + +# ── tests ───────────────────────────────────────────────────────────────────── + +def test_sync_handler_is_not_a_coroutine(): + """Sync handlers should work without any awaiting.""" + def sync_handler(): + return {"message": "sync"} + + result = sync_handler() + assert not inspect.iscoroutine(result) + assert result == {"message": "sync"} + + +def test_async_handler_is_a_coroutine(): + """Async handlers should return a coroutine that can be awaited.""" + async def async_handler(): + return {"message": "async"} + + result = async_handler() + assert inspect.iscoroutine(result) + final = asyncio.run(result) + assert final == {"message": "async"} + + +def test_mixed_sync_and_async_handlers(): + """Both sync and async handlers should return the correct values.""" + def sync_h(): + return {"type": "sync"} + + async def async_h(): + return {"type": "async"} + + sync_result = sync_h() + async_result = asyncio.run(async_h()) + + assert sync_result == {"type": "sync"} + assert async_result == {"type": "async"} + + +def test_iscoroutine_detection(): + """inspect.iscoroutine() correctly distinguishes sync vs async results.""" + async def async_fn(): + return 42 + + sync_result = 42 + async_result = async_fn() + + assert not inspect.iscoroutine(sync_result) + assert inspect.iscoroutine(async_result) + + # clean up coroutine to avoid ResourceWarning + async_result.close() From 5e89827503bcbb9ac95bd6a49c24e42bf329790a Mon Sep 17 00:00:00 2001 From: hirenkumar-n-dholariya Date: Fri, 17 Apr 2026 07:21:45 -0400 Subject: [PATCH 4/8] refactor(event-handler): move _registered_api_adapter_async to async_utils.py Per maintainer feedback on #8157 - function belongs in async_utils.py alongside other async event handler internals (wrap_middleware_async, _run_sync_middleware_in_thread, etc.) Signed-off-by: hirenkumar-n-dholariya --- .../event_handler/middlewares/async_utils.py | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/middlewares/async_utils.py b/aws_lambda_powertools/event_handler/middlewares/async_utils.py index b04db33f1e8..04aa3a86d39 100644 --- a/aws_lambda_powertools/event_handler/middlewares/async_utils.py +++ b/aws_lambda_powertools/event_handler/middlewares/async_utils.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, Response + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, BedrockResponse, Response def wrap_middleware_async(middleware: Callable, next_handler: Callable) -> Callable: @@ -105,3 +105,56 @@ def run_middleware() -> None: raise middleware_error_holder[0] return middleware_result_holder[0] + +async def _registered_api_adapter_async( + app: "ApiGatewayResolver", + next_middleware: Callable[..., Any], +) -> "dict | tuple | Response | BedrockResponse": + """ + Async version of _registered_api_adapter. + + Detects if the route handler is a coroutine and awaits it. + _to_response() stays sync (CPU-bound — no async benefit). + + IMPORTANT: This is an internal building block only. + Nothing calls it in the resolve chain yet. It will be used + by resolve_async() (see issue #8137). + + Parameters + ---------- + app: ApiGatewayResolver + The API Gateway resolver + next_middleware: Callable[..., Any] + The function to handle the API + + Returns + ------- + Response + The API Response Object + """ + route_args: dict = app.context.get("_route_args", {}) + + route = app.context.get("_route") + if route is not None: + if not route.request_param_name_checked: + from aws_lambda_powertools.event_handler.api_gateway import _find_request_param_name + route.request_param_name = _find_request_param_name(next_middleware) + route.request_param_name_checked = True + if route.request_param_name: + route_args = {**route_args, route.request_param_name: app.request} + + if route.has_dependencies: + from aws_lambda_powertools.event_handler.depends import build_dependency_tree, solve_dependencies + dep_values = solve_dependencies( + dependant=build_dependency_tree(route.func), + request=app.request, + dependency_overrides=app.dependency_overrides or None, + ) + route_args.update(dep_values) + + # Call handler — detect if result is a coroutine and await it + result = next_middleware(**route_args) + if inspect.iscoroutine(result): + result = await result + + return app._to_response(result) From 2063c218910f4acf5059ee7b5898543666fb71e0 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Sun, 19 Apr 2026 10:08:41 +0100 Subject: [PATCH 5/8] fix: small changes --- .../event_handler/middlewares/async_utils.py | 7 +- .../test_registered_api_adapter_async.py | 72 ------ .../test_registered_api_adapter_async.py | 214 ++++++++++++++++++ 3 files changed, 219 insertions(+), 74 deletions(-) delete mode 100644 aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py create mode 100644 tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py diff --git a/aws_lambda_powertools/event_handler/middlewares/async_utils.py b/aws_lambda_powertools/event_handler/middlewares/async_utils.py index 04aa3a86d39..7996c5e60d5 100644 --- a/aws_lambda_powertools/event_handler/middlewares/async_utils.py +++ b/aws_lambda_powertools/event_handler/middlewares/async_utils.py @@ -106,10 +106,11 @@ def run_middleware() -> None: return middleware_result_holder[0] + async def _registered_api_adapter_async( - app: "ApiGatewayResolver", + app: ApiGatewayResolver, next_middleware: Callable[..., Any], -) -> "dict | tuple | Response | BedrockResponse": +) -> dict | tuple | Response | BedrockResponse: """ Async version of _registered_api_adapter. @@ -138,6 +139,7 @@ async def _registered_api_adapter_async( if route is not None: if not route.request_param_name_checked: from aws_lambda_powertools.event_handler.api_gateway import _find_request_param_name + route.request_param_name = _find_request_param_name(next_middleware) route.request_param_name_checked = True if route.request_param_name: @@ -145,6 +147,7 @@ async def _registered_api_adapter_async( if route.has_dependencies: from aws_lambda_powertools.event_handler.depends import build_dependency_tree, solve_dependencies + dep_values = solve_dependencies( dependant=build_dependency_tree(route.func), request=app.request, diff --git a/aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py b/aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py deleted file mode 100644 index c1b02e9731a..00000000000 --- a/aws_lambda_powertools/event_handler/test_registered_api_adapter_async.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Unit tests for _registered_api_adapter_async() -Covers: sync handler, async handler, and mixed scenarios -""" -import asyncio -import inspect -import pytest -from unittest.mock import MagicMock - - -# ── helpers ────────────────────────────────────────────────────────────────── - -def _make_app(route_args=None, route=None): - """Build a minimal mock app context.""" - app = MagicMock() - app.context = {"_route_args": route_args or {}, "_route": route} - app.request = MagicMock() - app._to_response = lambda result: result # pass-through for testing - return app - - -# ── tests ───────────────────────────────────────────────────────────────────── - -def test_sync_handler_is_not_a_coroutine(): - """Sync handlers should work without any awaiting.""" - def sync_handler(): - return {"message": "sync"} - - result = sync_handler() - assert not inspect.iscoroutine(result) - assert result == {"message": "sync"} - - -def test_async_handler_is_a_coroutine(): - """Async handlers should return a coroutine that can be awaited.""" - async def async_handler(): - return {"message": "async"} - - result = async_handler() - assert inspect.iscoroutine(result) - final = asyncio.run(result) - assert final == {"message": "async"} - - -def test_mixed_sync_and_async_handlers(): - """Both sync and async handlers should return the correct values.""" - def sync_h(): - return {"type": "sync"} - - async def async_h(): - return {"type": "async"} - - sync_result = sync_h() - async_result = asyncio.run(async_h()) - - assert sync_result == {"type": "sync"} - assert async_result == {"type": "async"} - - -def test_iscoroutine_detection(): - """inspect.iscoroutine() correctly distinguishes sync vs async results.""" - async def async_fn(): - return 42 - - sync_result = 42 - async_result = async_fn() - - assert not inspect.iscoroutine(sync_result) - assert inspect.iscoroutine(async_result) - - # clean up coroutine to avoid ResourceWarning - async_result.close() diff --git a/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py new file mode 100644 index 00000000000..5587cc1e2fa --- /dev/null +++ b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import asyncio +from typing import cast + +import pytest + +from aws_lambda_powertools.event_handler import content_types +from aws_lambda_powertools.event_handler.api_gateway import ( + APIGatewayHttpResolver, + ApiGatewayResolver, + APIGatewayRestResolver, + BaseRouter, + ProxyEventType, + Response, +) +from aws_lambda_powertools.event_handler.middlewares.async_utils import _registered_api_adapter_async +from tests.functional.utils import load_event + +API_REST_EVENT = load_event("apiGatewayProxyEvent.json") +API_RESTV2_EVENT = load_event("apiGatewayProxyV2Event_GET.json") + + +def _setup_resolver_context(app: ApiGatewayResolver, event: dict) -> None: + """Populate the resolver context the same way resolve() does, without calling the full chain.""" + BaseRouter.current_event = app._to_proxy_event(cast(dict, event)) + BaseRouter.lambda_context = {} + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_sync_handler_returns_response(app: ApiGatewayResolver, event): + # GIVEN a sync route handler + @app.get("/my/path") + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "sync response") + + # WHEN resolving the event through the normal chain + result = app(event, {}) + + # THEN the sync handler is called and returns correctly + assert result["statusCode"] == 200 + assert result["body"] == "sync response" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_async_handler_is_awaited(app: ApiGatewayResolver, event): + # GIVEN an async route handler registered on the resolver + @app.get("/my/path") + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "async response") + + # WHEN populating context and calling the async adapter directly + _setup_resolver_context(app, event) + app.append_context(_route_args={}) + + result = asyncio.get_event_loop().run_until_complete( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the async handler is awaited and returns correctly + assert result.status_code == 200 + assert result.body == "async response" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_sync_handler_through_adapter(app: ApiGatewayResolver, event): + # GIVEN a sync route handler + @app.get("/my/path") + def get_lambda(): + return Response(200, content_types.TEXT_HTML, "sync via adapter") + + # WHEN calling _registered_api_adapter_async with a sync handler + _setup_resolver_context(app, event) + app.append_context(_route_args={}) + + result = asyncio.get_event_loop().run_until_complete( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN sync handler works through the async adapter without issue + assert result.status_code == 200 + assert result.body == "sync via adapter" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_adapter_passes_route_args_to_async_handler(app: ApiGatewayResolver, event): + # GIVEN an async handler that expects route arguments + async def get_lambda(name: str): + return Response(200, content_types.TEXT_HTML, name) + + # WHEN route_args are set in the context + _setup_resolver_context(app, event) + app.append_context(_route_args={"name": "powertools"}) + + result = asyncio.get_event_loop().run_until_complete( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the route args are passed to the handler + assert result.status_code == 200 + assert result.body == "powertools" + + +@pytest.mark.parametrize( + "app, event", + [ + (ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT), + (APIGatewayRestResolver(), API_REST_EVENT), + (APIGatewayHttpResolver(), API_RESTV2_EVENT), + ], +) +def test_adapter_passes_route_args_to_sync_handler(app: ApiGatewayResolver, event): + # GIVEN a sync handler that expects route arguments + def get_lambda(name: str): + return Response(200, content_types.TEXT_HTML, name) + + # WHEN route_args are set in the context + _setup_resolver_context(app, event) + app.append_context(_route_args={"name": "powertools"}) + + result = asyncio.get_event_loop().run_until_complete( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the route args are passed to the sync handler + assert result.status_code == 200 + assert result.body == "powertools" + + +def test_adapter_converts_dict_response_from_async_handler(): + # GIVEN an async handler that returns a dict (not a Response object) + app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent) + + async def get_lambda(): + return {"message": "hello"} + + # WHEN calling through the async adapter + _setup_resolver_context(app, API_REST_EVENT) + app.append_context(_route_args={}) + + result = asyncio.get_event_loop().run_until_complete( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN _to_response normalizes the dict into a Response object + assert result.status_code == 200 + assert result.body is not None + + +def test_adapter_converts_tuple_response_from_async_handler(): + # GIVEN an async handler that returns a (dict, status_code) tuple + app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent) + + async def get_lambda(): + return {"created": True}, 201 + + # WHEN calling through the async adapter + _setup_resolver_context(app, API_REST_EVENT) + app.append_context(_route_args={}) + + result = asyncio.get_event_loop().run_until_complete( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN _to_response normalizes the tuple into a Response object + assert result.status_code == 201 + + +def test_adapter_with_no_route_in_context(): + # GIVEN a handler and no _route in context + app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent) + + async def get_lambda(): + return Response(200, content_types.TEXT_HTML, "no route") + + # WHEN _route is None in context (default) + _setup_resolver_context(app, API_REST_EVENT) + app.append_context(_route_args={}) + + result = asyncio.get_event_loop().run_until_complete( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the adapter skips request injection and dependency resolution + assert result.status_code == 200 + assert result.body == "no route" From d13e5a8de1288622e29061916e5e2fbacba56f94 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Sun, 19 Apr 2026 10:10:57 +0100 Subject: [PATCH 6/8] fix: small changes --- .../event_handler/middlewares/async_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aws_lambda_powertools/event_handler/middlewares/async_utils.py b/aws_lambda_powertools/event_handler/middlewares/async_utils.py index 7996c5e60d5..469ed1e96b1 100644 --- a/aws_lambda_powertools/event_handler/middlewares/async_utils.py +++ b/aws_lambda_powertools/event_handler/middlewares/async_utils.py @@ -4,9 +4,12 @@ import asyncio import inspect +import logging import threading from typing import TYPE_CHECKING, Any +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from collections.abc import Callable @@ -134,6 +137,7 @@ async def _registered_api_adapter_async( The API Response Object """ route_args: dict = app.context.get("_route_args", {}) + logger.debug(f"Calling API Route Handler: {route_args}") route = app.context.get("_route") if route is not None: From 47716bf9d763c55c2d7e97c6827712e17cc1e867 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Sun, 19 Apr 2026 10:36:21 +0100 Subject: [PATCH 7/8] fix: small changes --- .../test_registered_api_adapter_async.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py index 5587cc1e2fa..f1d0507fe97 100644 --- a/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py +++ b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py @@ -67,7 +67,7 @@ async def get_lambda(): _setup_resolver_context(app, event) app.append_context(_route_args={}) - result = asyncio.get_event_loop().run_until_complete( + result = asyncio.run( _registered_api_adapter_async(app, get_lambda), ) @@ -94,7 +94,7 @@ def get_lambda(): _setup_resolver_context(app, event) app.append_context(_route_args={}) - result = asyncio.get_event_loop().run_until_complete( + result = asyncio.run( _registered_api_adapter_async(app, get_lambda), ) @@ -120,7 +120,7 @@ async def get_lambda(name: str): _setup_resolver_context(app, event) app.append_context(_route_args={"name": "powertools"}) - result = asyncio.get_event_loop().run_until_complete( + result = asyncio.run( _registered_api_adapter_async(app, get_lambda), ) @@ -146,7 +146,7 @@ def get_lambda(name: str): _setup_resolver_context(app, event) app.append_context(_route_args={"name": "powertools"}) - result = asyncio.get_event_loop().run_until_complete( + result = asyncio.run( _registered_api_adapter_async(app, get_lambda), ) @@ -166,7 +166,7 @@ async def get_lambda(): _setup_resolver_context(app, API_REST_EVENT) app.append_context(_route_args={}) - result = asyncio.get_event_loop().run_until_complete( + result = asyncio.run( _registered_api_adapter_async(app, get_lambda), ) @@ -186,7 +186,7 @@ async def get_lambda(): _setup_resolver_context(app, API_REST_EVENT) app.append_context(_route_args={}) - result = asyncio.get_event_loop().run_until_complete( + result = asyncio.run( _registered_api_adapter_async(app, get_lambda), ) @@ -205,7 +205,7 @@ async def get_lambda(): _setup_resolver_context(app, API_REST_EVENT) app.append_context(_route_args={}) - result = asyncio.get_event_loop().run_until_complete( + result = asyncio.run( _registered_api_adapter_async(app, get_lambda), ) From b83e1901b9d6534b286ebbdbf9a7e3b02542109a Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Sun, 19 Apr 2026 12:02:11 +0100 Subject: [PATCH 8/8] fix: small changes --- .../test_registered_api_adapter_async.py | 125 +++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py index f1d0507fe97..10d5b4602f0 100644 --- a/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py +++ b/tests/functional/event_handler/required_dependencies/test_registered_api_adapter_async.py @@ -1,9 +1,9 @@ -from __future__ import annotations - import asyncio +import re from typing import cast import pytest +from typing_extensions import Annotated from aws_lambda_powertools.event_handler import content_types from aws_lambda_powertools.event_handler.api_gateway import ( @@ -13,8 +13,11 @@ BaseRouter, ProxyEventType, Response, + Route, ) +from aws_lambda_powertools.event_handler.depends import Depends from aws_lambda_powertools.event_handler.middlewares.async_utils import _registered_api_adapter_async +from aws_lambda_powertools.event_handler.request import Request from tests.functional.utils import load_event API_REST_EVENT = load_event("apiGatewayProxyEvent.json") @@ -212,3 +215,121 @@ async def get_lambda(): # THEN the adapter skips request injection and dependency resolution assert result.status_code == 200 assert result.body == "no route" + + +def test_adapter_injects_request_param(): + # GIVEN an async handler that declares a Request parameter + app = APIGatewayHttpResolver() + + async def get_lambda(request: Request): + return Response(200, content_types.TEXT_HTML, request.method) + + # WHEN a Route is present in context with request_param_name not yet checked + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + app.append_context(_route=route, _route_args={}) + + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN the Request object is injected and request_param_name is cached + assert result.status_code == 200 + assert route.request_param_name_checked is True + assert route.request_param_name == "request" + + +def test_adapter_uses_cached_request_param_name(): + # GIVEN a Route where request_param_name was already resolved + app = APIGatewayHttpResolver() + + async def get_lambda(req: Request): + return Response(200, content_types.TEXT_HTML, req.method) + + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + route.request_param_name = "req" + route.request_param_name_checked = True + app.append_context(_route=route, _route_args={}) + + # WHEN calling the adapter a second time (cache hit) + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN it still injects the Request using the cached param name + assert result.status_code == 200 + + +def test_adapter_resolves_dependencies(): + # GIVEN an async handler with Depends() parameters + app = APIGatewayHttpResolver() + + def get_greeting() -> str: + return "hello" + + async def get_lambda(greeting: Annotated[str, Depends(get_greeting)]): + return {"greeting": greeting} + + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + app.append_context(_route=route, _route_args={}) + + # WHEN calling the adapter + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN dependencies are resolved and injected + assert result.status_code == 200 + + +def test_adapter_resolves_dependencies_with_sync_handler(): + # GIVEN a sync handler with Depends() parameters + app = APIGatewayHttpResolver() + + def get_greeting() -> str: + return "hello" + + def get_lambda(greeting: Annotated[str, Depends(get_greeting)]): + return {"greeting": greeting} + + _setup_resolver_context(app, API_RESTV2_EVENT) + route = Route( + method="GET", + path="/my/path", + rule=re.compile(r"^/my/path$"), + func=get_lambda, + cors=False, + compress=False, + ) + app.append_context(_route=route, _route_args={}) + + # WHEN calling the adapter with a sync handler that has dependencies + result = asyncio.run( + _registered_api_adapter_async(app, get_lambda), + ) + + # THEN dependencies are resolved and injected for sync handler too + assert result.status_code == 200