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/4] 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/4] 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/4] 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/4] 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)