Skip to content

Commit cd8829b

Browse files
feat(event_handler): enrich request object (#8153)
1 parent 6bcb8a3 commit cd8829b

File tree

7 files changed

+342
-49
lines changed

7 files changed

+342
-49
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,7 @@ def my_middleware(app, next_middleware):
13271327
route_path=route.openapi_path,
13281328
path_parameters=self.context.get("_route_args", {}),
13291329
current_event=self.current_event,
1330+
context=self.context,
13301331
)
13311332
self.context["_request"] = request
13321333
return request

aws_lambda_powertools/event_handler/request.py

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,35 @@ class Request:
1212
"""Represents the resolved HTTP request.
1313
1414
Provides structured access to the matched route pattern, extracted path parameters,
15-
HTTP method, headers, query parameters, and body. Available via ``app.request``
16-
inside middleware and, when added as a type-annotated parameter, inside route handlers.
15+
HTTP method, headers, query parameters, body, the full Powertools proxy event
16+
(``resolved_event``), and the shared resolver context (``context``).
17+
18+
Available via ``app.request`` inside middleware and, when added as a type-annotated
19+
parameter, inside ``Depends()`` dependency functions and route handlers.
1720
1821
Examples
1922
--------
23+
**Dependency injection with Depends()**
24+
25+
```python
26+
from typing import Annotated
27+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request, Depends
28+
29+
app = APIGatewayRestResolver()
30+
31+
def get_auth_user(request: Request) -> str:
32+
# Full event access via resolved_event
33+
token = request.resolved_event.get_header_value("authorization", default_value="")
34+
user = validate_token(token)
35+
# Bridge with middleware via shared context
36+
request.context["user"] = user
37+
return user
38+
39+
@app.get("/orders")
40+
def list_orders(user: Annotated[str, Depends(get_auth_user)]):
41+
return {"user": user}
42+
```
43+
2044
**Middleware usage**
2145
2246
```python
@@ -39,32 +63,21 @@ def auth_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware
3963
4064
app.use(middlewares=[auth_middleware])
4165
```
42-
43-
**Route handler injection (type-annotated)**
44-
45-
```python
46-
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request
47-
48-
app = APIGatewayRestResolver()
49-
50-
@app.get("/applications/<application_id>")
51-
def get_application(application_id: str, request: Request):
52-
user_agent = request.headers.get("user-agent")
53-
return {"id": application_id, "user_agent": user_agent}
54-
```
5566
"""
5667

57-
__slots__ = ("_current_event", "_path_parameters", "_route_path")
68+
__slots__ = ("_context", "_current_event", "_path_parameters", "_route_path")
5869

5970
def __init__(
6071
self,
6172
route_path: str,
6273
path_parameters: dict[str, Any],
6374
current_event: BaseProxyEvent,
75+
context: dict[str, Any] | None = None,
6476
) -> None:
6577
self._route_path = route_path
6678
self._path_parameters = path_parameters
6779
self._current_event = current_event
80+
self._context = context if context is not None else {}
6881

6982
@property
7083
def route(self) -> str:
@@ -113,3 +126,45 @@ def body(self) -> str | None:
113126
def json_body(self) -> Any:
114127
"""Request body deserialized as a Python object (dict / list), or ``None``."""
115128
return self._current_event.json_body
129+
130+
@property
131+
def resolved_event(self) -> BaseProxyEvent:
132+
"""Full Powertools proxy event with all helpers and properties.
133+
134+
Provides access to the complete ``BaseProxyEvent`` (or subclass) that
135+
Powertools resolved for the current invocation. This includes cookies,
136+
request context, path, and event-source-specific properties that are not
137+
available through the convenience properties on :class:`Request`.
138+
139+
Examples
140+
--------
141+
```python
142+
def get_request_details(request: Request) -> dict:
143+
event = request.resolved_event
144+
return {
145+
"path": event.path,
146+
"cookies": event.cookies,
147+
"request_context": event.request_context,
148+
}
149+
```
150+
"""
151+
return self._current_event
152+
153+
@property
154+
def context(self) -> dict[str, Any]:
155+
"""Shared resolver context (``app.context``) for this invocation.
156+
157+
Provides read/write access to the same ``dict`` that middleware and
158+
``app.append_context()`` populate. This enables incremental migration
159+
from middleware-based data sharing to ``Depends()``-based injection:
160+
middleware writes to ``app.context``, dependencies read from
161+
``request.context``.
162+
163+
Examples
164+
--------
165+
```python
166+
def get_current_user(request: Request) -> dict:
167+
return request.context["user"]
168+
```
169+
"""
170+
return self._context

docs/core/event_handler/api_gateway.md

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,37 @@ You can use `append_context` when you want to share data between your App and Ro
13651365
--8<-- "examples/event_handler_rest/src/split_route_append_context_module.py"
13661366
```
13671367

1368+
#### Sample layout
1369+
1370+
This is a sample project layout for a monolithic function with routes split in different files (`/todos`, `/health`).
1371+
1372+
```shell hl_lines="4 7 10 12-13" title="Sample project layout"
1373+
.
1374+
├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc.
1375+
├── poetry.lock
1376+
├── src
1377+
│ ├── __init__.py
1378+
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt
1379+
│ └── todos
1380+
│ ├── __init__.py
1381+
│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base
1382+
│ └── routers # routers module
1383+
│ ├── __init__.py
1384+
│ ├── health.py # /health routes. from routers import todos; health.router
1385+
│ └── todos.py # /todos routes. from .routers import todos; todos.router
1386+
├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler
1387+
└── tests
1388+
├── __init__.py
1389+
├── unit
1390+
│ ├── __init__.py
1391+
│ └── test_todos.py # unit tests for the todos router
1392+
│ └── test_health.py # unit tests for the health router
1393+
└── functional
1394+
├── __init__.py
1395+
├── conftest.py # pytest fixtures for the functional tests
1396+
└── test_main.py # functional tests for the main lambda handler
1397+
```
1398+
13681399
### Dependency injection
13691400

13701401
You can use `Depends()` to declare dependencies that are automatically resolved and injected into your route handlers. This provides type-safe, composable, and testable dependency injection.
@@ -1389,10 +1420,36 @@ Dependencies can depend on other dependencies, forming a composable tree. Shared
13891420

13901421
Dependencies that need access to the current request can declare a parameter typed as `Request`. It will be injected automatically.
13911422

1392-
```python hl_lines="5-6 12 20"
1423+
The `Request` object provides:
1424+
1425+
* **`headers`**, **`query_parameters`**, **`body`**, **`json_body`**: common request data
1426+
* **`resolved_event`**: the full Powertools proxy event with all helpers, cookies, request context, and path
1427+
* **`context`**: shared resolver context (`app.context`) for bridging data between middleware and dependencies
1428+
1429+
```python hl_lines="5-6 14 20"
13931430
--8<-- "examples/event_handler_rest/src/dependency_injection_with_request.py"
13941431
```
13951432

1433+
#### Combining middleware and Depends()
1434+
1435+
Middleware and `Depends()` are **complementary patterns**. Use middleware for request interception (auth gates, redirects, response modification) and `Depends()` for typed data injection.
1436+
1437+
The bridge between them is `request.context`: middleware writes to `app.context`, and dependencies read from `request.context`:
1438+
1439+
```python hl_lines="12-18 22-23 27"
1440+
--8<-- "examples/event_handler_rest/src/dependency_injection_with_middleware.py"
1441+
```
1442+
1443+
???+ tip "When to use middleware vs Depends()"
1444+
| Use case | Middleware | Depends() |
1445+
| --- | --- | --- |
1446+
| Return custom HTTP responses (redirects, 401s) | **Yes** | No, can only return values or raise exceptions |
1447+
| Short-circuit the request pipeline | **Yes** | No |
1448+
| Pre/post-process responses (add headers, compress) | **Yes** | No |
1449+
| Inject typed, testable data into handlers | No | **Yes** |
1450+
| Compose a dependency tree with caching | No | **Yes** |
1451+
| Override dependencies in tests | No | **Yes**, via `dependency_overrides` |
1452+
13961453
#### Testing with dependency overrides
13971454

13981455
Use `dependency_overrides` to replace any dependency with a mock or stub during testing - no monkeypatching needed.
@@ -1407,37 +1464,6 @@ Use `dependency_overrides` to replace any dependency with a mock or stub during
14071464
???+ info "`append_context` vs `Depends()`"
14081465
`append_context` remains available for backward compatibility. `Depends()` is recommended for new code because it provides type safety, IDE autocomplete, composable dependency trees, and `dependency_overrides` for testing.
14091466

1410-
#### Sample layout
1411-
1412-
This is a sample project layout for a monolithic function with routes split in different files (`/todos`, `/health`).
1413-
1414-
```shell hl_lines="4 7 10 12-13" title="Sample project layout"
1415-
.
1416-
├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc.
1417-
├── poetry.lock
1418-
├── src
1419-
│ ├── __init__.py
1420-
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt
1421-
│ └── todos
1422-
│ ├── __init__.py
1423-
│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base
1424-
│ └── routers # routers module
1425-
│ ├── __init__.py
1426-
│ ├── health.py # /health routes. from routers import todos; health.router
1427-
│ └── todos.py # /todos routes. from .routers import todos; todos.router
1428-
├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler
1429-
└── tests
1430-
├── __init__.py
1431-
├── unit
1432-
│ ├── __init__.py
1433-
│ └── test_todos.py # unit tests for the todos router
1434-
│ └── test_health.py # unit tests for the health router
1435-
└── functional
1436-
├── __init__.py
1437-
├── conftest.py # pytest fixtures for the functional tests
1438-
└── test_main.py # functional tests for the main lambda handler
1439-
```
1440-
14411467
### Considerations
14421468

14431469
This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing_extensions import Annotated
2+
3+
from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response
4+
from aws_lambda_powertools.event_handler.depends import Depends
5+
from aws_lambda_powertools.event_handler.request import Request
6+
from aws_lambda_powertools.utilities.typing import LambdaContext
7+
8+
app = APIGatewayHttpResolver()
9+
10+
11+
# Middleware handles auth — it can return HTTP responses (redirects, 401s)
12+
def auth_middleware(app, next_middleware):
13+
token = app.current_event.headers.get("authorization", "")
14+
if not token:
15+
return Response(status_code=401, body="Unauthorized")
16+
17+
# Middleware writes to app.context
18+
app.append_context(user={"id": "user-123", "role": "admin"})
19+
return next_middleware(app)
20+
21+
22+
app.use(middlewares=[auth_middleware])
23+
24+
25+
# Depends() reads what middleware wrote via request.context — typed and testable
26+
def get_current_user(request: Request) -> dict:
27+
return request.context["user"]
28+
29+
30+
@app.get("/admin/dashboard")
31+
def admin_dashboard(user: Annotated[dict, Depends(get_current_user)]):
32+
return {"message": f"Welcome {user['id']}", "role": user["role"]}
33+
34+
35+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
36+
return app.resolve(event, context)

examples/event_handler_rest/src/dependency_injection_with_request.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111

1212
def get_authenticated_user(request: Request) -> str:
13-
user_id = request.headers.get("x-user-id")
13+
# Use resolved_event for full Powertools event access (cookies, request_context, path, etc.)
14+
user_id = request.resolved_event.headers.get("x-user-id", "")
1415
if not user_id:
1516
raise UnauthorizedError("Missing authentication")
1617
return user_id

tests/functional/event_handler/required_dependencies/test_depends.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,96 @@ def handler(val: Annotated[str, Depends(broken_dep)]):
414414
result = app(API_GW_V2_EVENT, {})
415415
assert result["statusCode"] == 200
416416
assert json.loads(result["body"]) == {"val": "it-works"}
417+
418+
419+
# ---------------------------------------------------------------------------
420+
# request.context — bridge between middleware and Depends()
421+
# ---------------------------------------------------------------------------
422+
423+
424+
def test_depends_request_context_writable():
425+
"""Dependencies can write to request.context and handlers can read it."""
426+
app = APIGatewayHttpResolver()
427+
428+
def set_tenant(request: Request) -> str:
429+
tenant = request.headers.get("x-tenant-id", "default")
430+
request.context["tenant"] = tenant
431+
return tenant
432+
433+
@app.post("/my/path")
434+
def handler(tenant: Annotated[str, Depends(set_tenant)], request: Request):
435+
return {"tenant": tenant, "from_context": request.context.get("tenant")}
436+
437+
event = {**API_GW_V2_EVENT, "headers": {**API_GW_V2_EVENT.get("headers", {}), "x-tenant-id": "acme-corp"}}
438+
result = app(event, {})
439+
440+
assert result["statusCode"] == 200
441+
body = json.loads(result["body"])
442+
assert body["tenant"] == "acme-corp"
443+
assert body["from_context"] == "acme-corp"
444+
445+
446+
def test_depends_request_context_bridges_middleware():
447+
"""Middleware writes to app.context, Depends() reads via request.context."""
448+
app = APIGatewayHttpResolver()
449+
450+
def auth_middleware(app, next_middleware):
451+
app.append_context(user="admin-user")
452+
return next_middleware(app)
453+
454+
app.use(middlewares=[auth_middleware])
455+
456+
def get_current_user(request: Request) -> str:
457+
return request.context["user"]
458+
459+
@app.post("/my/path")
460+
def handler(user: Annotated[str, Depends(get_current_user)]):
461+
return {"user": user}
462+
463+
result = app(API_GW_V2_EVENT, {})
464+
assert result["statusCode"] == 200
465+
assert json.loads(result["body"]) == {"user": "admin-user"}
466+
467+
468+
def test_depends_request_context_with_router():
469+
"""request.context works when routes come from an included Router."""
470+
from aws_lambda_powertools.event_handler.api_gateway import Router
471+
472+
app = APIGatewayHttpResolver()
473+
router = Router()
474+
475+
def mw(app, next_middleware):
476+
app.append_context(role="admin")
477+
return next_middleware(app)
478+
479+
app.use(middlewares=[mw])
480+
481+
def get_role(request: Request) -> str:
482+
return request.context["role"]
483+
484+
@router.post("/my/path")
485+
def handler(role: Annotated[str, Depends(get_role)]):
486+
return {"role": role}
487+
488+
app.include_router(router)
489+
490+
result = app(API_GW_V2_EVENT, {})
491+
assert result["statusCode"] == 200
492+
assert json.loads(result["body"]) == {"role": "admin"}
493+
494+
495+
def test_depends_request_resolved_event():
496+
"""Dependencies can access the full event via request.resolved_event."""
497+
app = APIGatewayHttpResolver()
498+
499+
def get_path(request: Request) -> str:
500+
return request.resolved_event.path
501+
502+
@app.post("/my/path")
503+
def handler(path: Annotated[str, Depends(get_path)]):
504+
return {"path": path}
505+
506+
result = app(API_GW_V2_EVENT, {})
507+
assert result["statusCode"] == 200
508+
body = json.loads(result["body"])
509+
assert body["path"] == "/my/path"

0 commit comments

Comments
 (0)