Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,7 @@ def my_middleware(app, next_middleware):
route_path=route.openapi_path,
path_parameters=self.context.get("_route_args", {}),
current_event=self.current_event,
context=self.context,
)
self.context["_request"] = request
return request
Expand Down
87 changes: 71 additions & 16 deletions aws_lambda_powertools/event_handler/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,35 @@ class Request:
"""Represents the resolved HTTP request.

Provides structured access to the matched route pattern, extracted path parameters,
HTTP method, headers, query parameters, and body. Available via ``app.request``
inside middleware and, when added as a type-annotated parameter, inside route handlers.
HTTP method, headers, query parameters, body, the full Powertools proxy event
(``resolved_event``), and the shared resolver context (``context``).

Available via ``app.request`` inside middleware and, when added as a type-annotated
parameter, inside ``Depends()`` dependency functions and route handlers.

Examples
--------
**Dependency injection with Depends()**

```python
from typing import Annotated
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request, Depends

app = APIGatewayRestResolver()

def get_auth_user(request: Request) -> str:
# Full event access via resolved_event
token = request.resolved_event.get_header_value("authorization", default_value="")
user = validate_token(token)
# Bridge with middleware via shared context
request.context["user"] = user
return user

@app.get("/orders")
def list_orders(user: Annotated[str, Depends(get_auth_user)]):
return {"user": user}
```

**Middleware usage**

```python
Expand All @@ -39,32 +63,21 @@ def auth_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware

app.use(middlewares=[auth_middleware])
```

**Route handler injection (type-annotated)**

```python
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request

app = APIGatewayRestResolver()

@app.get("/applications/<application_id>")
def get_application(application_id: str, request: Request):
user_agent = request.headers.get("user-agent")
return {"id": application_id, "user_agent": user_agent}
```
"""

__slots__ = ("_current_event", "_path_parameters", "_route_path")
__slots__ = ("_context", "_current_event", "_path_parameters", "_route_path")

def __init__(
self,
route_path: str,
path_parameters: dict[str, Any],
current_event: BaseProxyEvent,
context: dict[str, Any] | None = None,
) -> None:
self._route_path = route_path
self._path_parameters = path_parameters
self._current_event = current_event
self._context = context if context is not None else {}

@property
def route(self) -> str:
Expand Down Expand Up @@ -113,3 +126,45 @@ def body(self) -> str | None:
def json_body(self) -> Any:
"""Request body deserialized as a Python object (dict / list), or ``None``."""
return self._current_event.json_body

@property
def resolved_event(self) -> BaseProxyEvent:
"""Full Powertools proxy event with all helpers and properties.

Provides access to the complete ``BaseProxyEvent`` (or subclass) that
Powertools resolved for the current invocation. This includes cookies,
request context, path, and event-source-specific properties that are not
available through the convenience properties on :class:`Request`.

Examples
--------
```python
def get_request_details(request: Request) -> dict:
event = request.resolved_event
return {
"path": event.path,
"cookies": event.cookies,
"request_context": event.request_context,
}
```
"""
return self._current_event

@property
def context(self) -> dict[str, Any]:
"""Shared resolver context (``app.context``) for this invocation.

Provides read/write access to the same ``dict`` that middleware and
``app.append_context()`` populate. This enables incremental migration
from middleware-based data sharing to ``Depends()``-based injection:
middleware writes to ``app.context``, dependencies read from
``request.context``.

Examples
--------
```python
def get_current_user(request: Request) -> dict:
return request.context["user"]
```
"""
return self._context
90 changes: 58 additions & 32 deletions docs/core/event_handler/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,37 @@ You can use `append_context` when you want to share data between your App and Ro
--8<-- "examples/event_handler_rest/src/split_route_append_context_module.py"
```

#### Sample layout

This is a sample project layout for a monolithic function with routes split in different files (`/todos`, `/health`).

```shell hl_lines="4 7 10 12-13" title="Sample project layout"
.
├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc.
├── poetry.lock
├── src
│ ├── __init__.py
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt
│ └── todos
│ ├── __init__.py
│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base
│ └── routers # routers module
│ ├── __init__.py
│ ├── health.py # /health routes. from routers import todos; health.router
│ └── todos.py # /todos routes. from .routers import todos; todos.router
├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler
└── tests
├── __init__.py
├── unit
│ ├── __init__.py
│ └── test_todos.py # unit tests for the todos router
│ └── test_health.py # unit tests for the health router
└── functional
├── __init__.py
├── conftest.py # pytest fixtures for the functional tests
└── test_main.py # functional tests for the main lambda handler
```

### Dependency injection

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.
Expand All @@ -1389,10 +1420,36 @@ Dependencies can depend on other dependencies, forming a composable tree. Shared

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

```python hl_lines="5-6 12 20"
The `Request` object provides:

* **`headers`**, **`query_parameters`**, **`body`**, **`json_body`**: common request data
* **`resolved_event`**: the full Powertools proxy event with all helpers, cookies, request context, and path
* **`context`**: shared resolver context (`app.context`) for bridging data between middleware and dependencies

```python hl_lines="5-6 14 20"
--8<-- "examples/event_handler_rest/src/dependency_injection_with_request.py"
```

#### Combining middleware and Depends()

Middleware and `Depends()` are **complementary patterns**. Use middleware for request interception (auth gates, redirects, response modification) and `Depends()` for typed data injection.

The bridge between them is `request.context`: middleware writes to `app.context`, and dependencies read from `request.context`:

```python hl_lines="12-18 22-23 27"
--8<-- "examples/event_handler_rest/src/dependency_injection_with_middleware.py"
```

???+ tip "When to use middleware vs Depends()"
| Use case | Middleware | Depends() |
| --- | --- | --- |
| Return custom HTTP responses (redirects, 401s) | **Yes** | No, can only return values or raise exceptions |
| Short-circuit the request pipeline | **Yes** | No |
| Pre/post-process responses (add headers, compress) | **Yes** | No |
| Inject typed, testable data into handlers | No | **Yes** |
| Compose a dependency tree with caching | No | **Yes** |
| Override dependencies in tests | No | **Yes**, via `dependency_overrides` |

#### Testing with dependency overrides

Use `dependency_overrides` to replace any dependency with a mock or stub during testing - no monkeypatching needed.
Expand All @@ -1407,37 +1464,6 @@ Use `dependency_overrides` to replace any dependency with a mock or stub during
???+ info "`append_context` vs `Depends()`"
`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.

#### Sample layout

This is a sample project layout for a monolithic function with routes split in different files (`/todos`, `/health`).

```shell hl_lines="4 7 10 12-13" title="Sample project layout"
.
├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc.
├── poetry.lock
├── src
│ ├── __init__.py
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt
│ └── todos
│ ├── __init__.py
│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base
│ └── routers # routers module
│ ├── __init__.py
│ ├── health.py # /health routes. from routers import todos; health.router
│ └── todos.py # /todos routes. from .routers import todos; todos.router
├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler
└── tests
├── __init__.py
├── unit
│ ├── __init__.py
│ └── test_todos.py # unit tests for the todos router
│ └── test_health.py # unit tests for the health router
└── functional
├── __init__.py
├── conftest.py # pytest fixtures for the functional tests
└── test_main.py # functional tests for the main lambda handler
```

### Considerations

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing_extensions import Annotated

from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response
from aws_lambda_powertools.event_handler.depends import Depends
from aws_lambda_powertools.event_handler.request import Request
from aws_lambda_powertools.utilities.typing import LambdaContext

app = APIGatewayHttpResolver()


# Middleware handles auth — it can return HTTP responses (redirects, 401s)
def auth_middleware(app, next_middleware):
token = app.current_event.headers.get("authorization", "")
if not token:
return Response(status_code=401, body="Unauthorized")

# Middleware writes to app.context
app.append_context(user={"id": "user-123", "role": "admin"})
return next_middleware(app)


app.use(middlewares=[auth_middleware])


# Depends() reads what middleware wrote via request.context — typed and testable
def get_current_user(request: Request) -> dict:
return request.context["user"]


@app.get("/admin/dashboard")
def admin_dashboard(user: Annotated[dict, Depends(get_current_user)]):
return {"message": f"Welcome {user['id']}", "role": user["role"]}


def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@


def get_authenticated_user(request: Request) -> str:
user_id = request.headers.get("x-user-id")
# Use resolved_event for full Powertools event access (cookies, request_context, path, etc.)
user_id = request.resolved_event.headers.get("x-user-id", "")
if not user_id:
raise UnauthorizedError("Missing authentication")
return user_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,96 @@ def handler(val: Annotated[str, Depends(broken_dep)]):
result = app(API_GW_V2_EVENT, {})
assert result["statusCode"] == 200
assert json.loads(result["body"]) == {"val": "it-works"}


# ---------------------------------------------------------------------------
# request.context — bridge between middleware and Depends()
# ---------------------------------------------------------------------------


def test_depends_request_context_writable():
"""Dependencies can write to request.context and handlers can read it."""
app = APIGatewayHttpResolver()

def set_tenant(request: Request) -> str:
tenant = request.headers.get("x-tenant-id", "default")
request.context["tenant"] = tenant
return tenant

@app.post("/my/path")
def handler(tenant: Annotated[str, Depends(set_tenant)], request: Request):
return {"tenant": tenant, "from_context": request.context.get("tenant")}

event = {**API_GW_V2_EVENT, "headers": {**API_GW_V2_EVENT.get("headers", {}), "x-tenant-id": "acme-corp"}}
result = app(event, {})

assert result["statusCode"] == 200
body = json.loads(result["body"])
assert body["tenant"] == "acme-corp"
assert body["from_context"] == "acme-corp"


def test_depends_request_context_bridges_middleware():
"""Middleware writes to app.context, Depends() reads via request.context."""
app = APIGatewayHttpResolver()

def auth_middleware(app, next_middleware):
app.append_context(user="admin-user")
return next_middleware(app)

app.use(middlewares=[auth_middleware])

def get_current_user(request: Request) -> str:
return request.context["user"]

@app.post("/my/path")
def handler(user: Annotated[str, Depends(get_current_user)]):
return {"user": user}

result = app(API_GW_V2_EVENT, {})
assert result["statusCode"] == 200
assert json.loads(result["body"]) == {"user": "admin-user"}


def test_depends_request_context_with_router():
"""request.context works when routes come from an included Router."""
from aws_lambda_powertools.event_handler.api_gateway import Router

app = APIGatewayHttpResolver()
router = Router()

def mw(app, next_middleware):
app.append_context(role="admin")
return next_middleware(app)

app.use(middlewares=[mw])

def get_role(request: Request) -> str:
return request.context["role"]

@router.post("/my/path")
def handler(role: Annotated[str, Depends(get_role)]):
return {"role": role}

app.include_router(router)

result = app(API_GW_V2_EVENT, {})
assert result["statusCode"] == 200
assert json.loads(result["body"]) == {"role": "admin"}


def test_depends_request_resolved_event():
"""Dependencies can access the full event via request.resolved_event."""
app = APIGatewayHttpResolver()

def get_path(request: Request) -> str:
return request.resolved_event.path

@app.post("/my/path")
def handler(path: Annotated[str, Depends(get_path)]):
return {"path": path}

result = app(API_GW_V2_EVENT, {})
assert result["statusCode"] == 200
body = json.loads(result["body"])
assert body["path"] == "/my/path"
Loading
Loading