diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index cd8fcc57791..b5b312e7a26 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -943,6 +943,50 @@ Here's a sample middleware that extracts and injects correlation ID, using `APIG --8<-- "examples/event_handler_rest/src/middleware_getting_started_output.json" ``` +#### Accessing the Request object + +After route resolution, Event Handler creates a `Request` object with the **resolved** route context. You can access it in two ways: + +1. **`app.request`** - available in middleware functions. +2. **`request: Request` type annotation** - injected automatically into route handlers as a parameter. + +`Request` gives you the resolved route context that `app.current_event` doesn't have: + +| Property | Example | Description | +| --------------------- | ---------------------------------------- | ----------------------------------------------------- | +| `route` | `/todos/{todo_id}` | Matched route pattern in OpenAPI path-template format | +| `path_parameters` | `{"todo_id": "123"}` | Powertools-resolved path parameters | +| `method` | `GET` | HTTP method (upper-case) | +| `headers` | `{"content-type": "application/json"}` | Request headers | +| `query_parameters` | `{"page": "1"}` | Query string parameters | +| `body` | `'{"name": "task"}'` | Raw request body | +| `json_body` | `{"name": "task"}` | Deserialized request body | + +=== "Using `app.request` in middleware" + + ```python hl_lines="10 13-15 21" title="Accessing Request via app.request" + --8<-- "examples/event_handler_rest/src/middleware_request_object.py" + ``` + + 1. Access the resolved `Request` object from the app instance. + 2. `request.route` returns the matched route pattern, e.g. `/todos/{todo_id}`. + 3. `request.path_parameters` returns the Powertools-resolved parameters, e.g. `{"todo_id": "123"}`. + 4. You can include route metadata in the response headers. + +=== "Using `request: Request` in route handlers" + + ```python hl_lines="7 10" title="Accessing Request via type annotation" + --8<-- "examples/event_handler_rest/src/middleware_request_handler_injection.py" + ``` + + 1. Add `request: Request` as a parameter - Event Handler injects it automatically. + 2. Access resolved route, path parameters, headers, query parameters, and body. + +???+ note "When to use `Request` vs `app.current_event`" + Use `Request` for **route-aware** logic like authorization, logging, and metrics - it gives you the matched route pattern and Powertools-resolved path parameters. + + Use `app.current_event` when you need the **raw event** data like request context, stage variables, or authorizer context that is available from the start of the request, before route resolution. + #### Global middlewares
@@ -1112,7 +1156,7 @@ Keep the following in mind when authoring middlewares for Event Handler: 2. **Call the next middleware**. Return the result of `next_middleware(app)`, or a [Response object](#fine-grained-responses) when you want to [return early](#returning-early). 3. **Keep a lean scope**. Focus on a single task per middleware to ease composability and maintenance. In [debug mode](#debug-mode), we also print out the order middlewares will be triggered to ease operations. 4. **Catch your own exceptions**. Catch and handle known exceptions to your logic. Unless you want to raise [HTTP Errors](#raising-http-errors), or propagate specific exceptions to the client. To catch all and any exceptions, we recommend you use the [exception_handler](#exception-handling) decorator. -5. **Use context to share data**. Use `app.append_context` to [share contextual data](#sharing-contextual-data) between middlewares and route handlers, and `app.context.get(key)` to fetch them. We clear all contextual data at the end of every request. +5. **Use context to share data**. Use `app.append_context` to [share contextual data](#sharing-contextual-data) between middlewares and route handlers, and `app.context.get(key)` to fetch them. We clear all contextual data at the end of every request. For route-aware request data, use [`app.request`](#accessing-the-request-object) instead. ### Fine grained responses diff --git a/examples/event_handler_rest/src/middleware_request_handler_injection.py b/examples/event_handler_rest/src/middleware_request_handler_injection.py new file mode 100644 index 00000000000..9304fcb1021 --- /dev/null +++ b/examples/event_handler_rest/src/middleware_request_handler_injection.py @@ -0,0 +1,16 @@ +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Request + +app = APIGatewayRestResolver() + + +@app.get("/todos/") +def get_todo(todo_id: str, request: Request): # (1)! + return { + "id": todo_id, + "route": request.route, # (2)! + "user_agent": request.headers.get("user-agent", ""), + } + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/middleware_request_object.py b/examples/event_handler_rest/src/middleware_request_object.py new file mode 100644 index 00000000000..9f58460888d --- /dev/null +++ b/examples/event_handler_rest/src/middleware_request_object.py @@ -0,0 +1,30 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response +from aws_lambda_powertools.event_handler.middlewares import NextMiddleware + +app = APIGatewayRestResolver() +logger = Logger() + + +def request_context_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + request = app.request # (1)! + + logger.append_keys( + route=request.route, # (2)! + method=request.method, + path_parameters=request.path_parameters, # (3)! + ) + + response = next_middleware(app) + + response.headers["x-route-pattern"] = request.route # (4)! + return response + + +@app.get("/todos/", middlewares=[request_context_middleware]) +def get_todo(todo_id: str): + return {"id": todo_id} + + +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/poetry.lock b/poetry.lock index f8d475fc473..37583604a7e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -11,7 +11,7 @@ files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [[package]] name = "anyio" @@ -325,7 +325,7 @@ description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"tracer\"" +markers = "extra == \"tracer\" or extra == \"all\"" files = [ {file = "aws_xray_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:422d62ad7d52e373eebb90b642eb1bb24657afe03b22a8df4a8b2e5108e278a3"}, {file = "aws_xray_sdk-2.15.0.tar.gz", hash = "sha256:794381b96e835314345068ae1dd3b9120bd8b4e21295066c37e8814dbb341365"}, @@ -1830,7 +1830,7 @@ description = "Fastest Python implementation of JSON schema" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"validation\"" +markers = "extra == \"validation\" or extra == \"all\"" files = [ {file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"}, {file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"}, @@ -1912,7 +1912,6 @@ python-versions = ">=3.10" groups = ["dev"] files = [ {file = "griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa"}, - {file = "griffe-2.0.0.tar.gz", hash = "sha256:c68979cd8395422083a51ea7cf02f9c119d889646d99b7b656ee43725de1b80f"}, ] [package.dependencies] @@ -1931,7 +1930,6 @@ python-versions = ">=3.10" groups = ["dev"] files = [ {file = "griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d"}, - {file = "griffecli-2.0.0.tar.gz", hash = "sha256:312fa5ebb4ce6afc786356e2d0ce85b06c1c20d45abc42d74f0cda65e159f6ef"}, ] [package.dependencies] @@ -1947,7 +1945,6 @@ python-versions = ">=3.10" groups = ["dev"] files = [ {file = "griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f"}, - {file = "griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934"}, ] [package.extras] @@ -3408,7 +3405,7 @@ files = [ {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [package.dependencies] annotated-types = ">=0.6.0" @@ -3550,7 +3547,7 @@ files = [ {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [package.dependencies] typing-extensions = ">=4.14.1" @@ -3597,14 +3594,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.21" +version = "10.21.2" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f"}, - {file = "pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5"}, + {file = "pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638"}, + {file = "pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc"}, ] [package.dependencies] @@ -4804,7 +4801,7 @@ files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, ] -markers = {main = "extra == \"all\" or extra == \"parser\""} +markers = {main = "extra == \"parser\" or extra == \"all\""} [package.dependencies] typing-extensions = ">=4.12.0" @@ -5122,7 +5119,7 @@ files = [ {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, ] -markers = {main = "extra == \"all\" or extra == \"datamasking\" or extra == \"tracer\" or extra == \"datadog\""} +markers = {main = "extra == \"tracer\" or extra == \"all\" or extra == \"datamasking\" or extra == \"datadog\""} [[package]] name = "xenon"