Skip to content

feat(event_handler): add Middleware support for REST Event Handler #2917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 92 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
fe2341f
feat(rest-middleware): initial work for rest API middleware
walmsles Aug 2, 2023
4cfa87d
feat(rest-middlware): Add in Router based middleware and add Middlewa…
walmsles Aug 3, 2023
4e28b06
feat(api-middleware): remove compression middleware - not implemented…
walmsles Aug 3, 2023
edcc6f7
fix(typing): resolve mypy errors
walmsles Aug 3, 2023
2c67e10
Update aws_lambda_powertools/event_handler/api_gateway.py
walmsles Aug 3, 2023
822f16b
Update aws_lambda_powertools/event_handler/api_gateway.py
walmsles Aug 3, 2023
51c3da5
Update aws_lambda_powertools/event_handler/api_gateway.py
walmsles Aug 3, 2023
98da7dc
Update aws_lambda_powertools/event_handler/api_gateway.py
walmsles Aug 3, 2023
d775bba
Update aws_lambda_powertools/event_handler/api_gateway.py
walmsles Aug 3, 2023
d12fa20
Update aws_lambda_powertools/event_handler/api_gateway.py
walmsles Aug 3, 2023
e765ebc
Apply suggestions from code review
walmsles Aug 3, 2023
6e54256
fix(naming-intent): rename middleware to middlewares universally in a…
walmsles Aug 3, 2023
727effb
fix(args-debt): rename args to route_arguments in all places
walmsles Aug 3, 2023
0afe31c
fix(middleware-clarity): Rename and repackage middleware construction…
walmsles Aug 4, 2023
8501b9a
fix(jsdocs): added docs to MiddlewareStackWrapper and BaseMiddlewareH…
walmsles Aug 4, 2023
0d59e12
fix(types): Mkae MiddlewareStackWrapper __call__ return type more spe…
walmsles Aug 4, 2023
6b9b369
fix(middlewares): internalise registered_api_adapter and remove Cors/…
walmsles Aug 5, 2023
409da32
feat(middlewares): Add chema validation middleware and internalise re…
walmsles Aug 5, 2023
1a90b34
chore(security): relabel http to https for test JSON Schema (because …
walmsles Aug 5, 2023
a9d9cb2
Apply suggestions from code review
walmsles Aug 10, 2023
10080b5
chore(typing): Ficup typing and function/attribute scopes - keep priv…
walmsles Aug 10, 2023
de7247d
feat(middlewares): Add optional outbound schema dn formats to schema …
walmsles Aug 10, 2023
dbae938
fix(errors): add logger for config exceptions and return minimal details
walmsles Aug 10, 2023
13df122
chore(docs): fixup format of docstrings to numpy format
walmsles Aug 10, 2023
8067116
chore(docs): change docstrings to numpy format
walmsles Aug 10, 2023
6ce64f1
fix(tests): fix failing tests due to resposne change:
walmsles Aug 11, 2023
22b83e0
chore(tests): group middleware testing to one module
walmsles Aug 11, 2023
af719f4
chore(docs): api_agteway - improve docs for middleware and examples
walmsles Aug 13, 2023
0adfc70
chore(docs): document why mypy 'type: ignore' is used here
walmsles Aug 13, 2023
63db815
feat(api_middleware): add source diagram for middlewares docs
walmsles Aug 13, 2023
3131f3e
fix(tests): fix unreachable code
walmsles Aug 13, 2023
1a20c9e
fix(debug): fix processed middleware stack frame list, cleanup try/ca…
walmsles Aug 15, 2023
48a9462
fix(debug): revert small change to ensure debug noise reduced via han…
walmsles Aug 15, 2023
587348c
chore(comments): cleanup middleware execution
walmsles Aug 19, 2023
deabb26
feat(docs): Add docs for event_handler middleware
walmsles Aug 20, 2023
fbb36ab
feat(docs): Tidy up middleware docs and add to section to split routes
walmsles Aug 20, 2023
d4fc1ba
feat(route): expose matched route instance to Resolver in additional …
walmsles Aug 22, 2023
837613b
fix(resolver): Add 'powertols_route' to additional context
walmsles Aug 22, 2023
9aec250
Merge branch 'develop' into feat/api-middleware
heitorlessa Aug 28, 2023
49537a9
docs: make intro punchy plus intro diagram
heitorlessa Aug 28, 2023
d0be0e4
docs: use correlation id as first middleware
heitorlessa Aug 28, 2023
2b6ee0b
docs: add output for completeness
heitorlessa Aug 28, 2023
33cf3ad
docs: grammar
heitorlessa Aug 28, 2023
40b68a0
docs: note that middleware works for any resolver
heitorlessa Aug 28, 2023
7bd0dba
docs: add global middlewares section
heitorlessa Aug 28, 2023
947d0ce
docs: additional ctx for global middlewares order
heitorlessa Aug 28, 2023
efee2ae
docs: add early return section
heitorlessa Aug 28, 2023
741f2a5
feat(route): expose route instance in ephemeral context dict
walmsles Aug 30, 2023
388592f
feat(middlewwares): Expand tests to incdlue core gateway types
walmsles Aug 30, 2023
af15705
feat(middlewares): Additional Tests for route middleware, added debug…
walmsles Aug 30, 2023
bf439e4
feat(middlewares): Tidy up config, add additional context for _path f…
walmsles Aug 30, 2023
80aa67e
Merge branch 'develop' into feat/api-middleware
walmsles Sep 1, 2023
9fb4c6f
chore(docs): updated imges to be svg for middlewares from draw.io - l…
walmsles Sep 1, 2023
06b1d90
chore(example): fix custom middleware example to correct raise of cap…
walmsles Sep 2, 2023
61022be
chore: type middleware callback
heitorlessa Sep 2, 2023
21af597
chore: type middleware response
heitorlessa Sep 2, 2023
0ca6683
docs: improve early return; use kwargs over ctx
heitorlessa Sep 2, 2023
9704939
docs: add handling exceptions section
heitorlessa Sep 2, 2023
d166de3
docs: fix media typo
heitorlessa Sep 2, 2023
f476f7d
docs: add being a good citizen section
heitorlessa Sep 3, 2023
e8a071e
docs: add skeleton for class-based middleware
heitorlessa Sep 3, 2023
dd59a7f
Merge branch 'develop' into feat/api-middleware
heitorlessa Sep 4, 2023
1c0977a
chore: test class based middlewares
heitorlessa Sep 4, 2023
7d361b6
docs: rename class based section
heitorlessa Sep 4, 2023
85e65b0
feat(middlewares): remove kwargs from middleware signature for cleane…
walmsles Sep 4, 2023
3a20318
feat(middlewares): align signatures for middlewareprocessing
walmsles Sep 4, 2023
5edde6a
feat(middlewares): cleanup typing for next callbacks
walmsles Sep 4, 2023
a0e784b
feat(middlewares): align typing for next_middlewares for consistency
walmsles Sep 4, 2023
f20b778
Merge branch 'develop' into feat/api-middleware
heitorlessa Sep 4, 2023
d8cf386
chore: use generics to accept any event handler
heitorlessa Sep 4, 2023
f5a613a
refactor: tech debt overload on get_header_value for middleware examp…
heitorlessa Sep 4, 2023
2f2eb91
chore: enforce protocol type checking for app instance
heitorlessa Sep 4, 2023
03708bb
docs: complete extending middlewares section
heitorlessa Sep 4, 2023
5dc5719
fix: remove schema validation export due to optional dependency
heitorlessa Sep 4, 2023
f7800b2
refactor: improve docstrings for schema validation; typing
heitorlessa Sep 4, 2023
6f63b4f
chore: leftover from previous circular dep issue
heitorlessa Sep 4, 2023
03d15fa
docs: add native middleware section
heitorlessa Sep 4, 2023
b7bcb08
docs - refactor references of with kwargs to , remove **lwargs refer…
walmsles Sep 4, 2023
7ca4a3a
chore: add middleware order test
heitorlessa Sep 5, 2023
870545a
refactor: add debug log
heitorlessa Sep 5, 2023
44cb04b
docs: move staging area to router section
heitorlessa Sep 5, 2023
3832817
Merge branch 'develop' into feat/api-middleware
heitorlessa Sep 5, 2023
4e1a654
fix: remove leftover order from middleware order test
heitorlessa Sep 5, 2023
6400b28
Merge branch 'develop' into feat/api-middleware
heitorlessa Sep 5, 2023
19eaa73
middlewares: reverse internal function rename to ensure no breaking c…
walmsles Sep 6, 2023
592d9da
chore(tests): remove typing isues
walmsles Sep 6, 2023
d1508c7
docs: middleware in router
heitorlessa Sep 7, 2023
aa68e40
chore: last cleanups
heitorlessa Sep 7, 2023
98948ec
docs: leftover to highlight works for micro/mono fns
heitorlessa Sep 7, 2023
20c344a
docs: fix highlighting after refactoring
heitorlessa Sep 7, 2023
4ed0076
chore: remove backup drawio file
heitorlessa Sep 7, 2023
7fbb132
Merge branch 'develop' into feat/api-middleware
leandrodamascena Sep 7, 2023
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
Prev Previous commit
Next Next commit
feat(middlewares): remove kwargs from middleware signature for cleane…
…r DX and relabler get_response to next_middleware for consistency
  • Loading branch information
walmsles committed Sep 4, 2023
commit 85e65b04a78c9fc35868ba339c0d27caadc626ed
50 changes: 24 additions & 26 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def __init__(
self.method = method.upper()
self.rule = rule
self.func = func
self._call_stack = func
self._middleware_stack = func
self.cors = cors
self.compress = compress
self.cache_control = cache_control
Expand Down Expand Up @@ -300,8 +300,11 @@ def __call__(
print("\n".join(getattr(item, "__name__", "Unknown") for item in all_middlewares))
print("=================")

# Call the Middleware Wrapped _call_stack function handler with the app and route arguments
return self._call_stack(app, **route_arguments)
# Add Route Arguments to app context
app.append_context(_route_args=route_arguments)

# Call the Middleware Wrapped _call_stack function handler with the app
return self._middleware_stack(app)

def _build_middleware_stack(self, router_middlewares: List[Callable]) -> None:
"""
Expand All @@ -327,12 +330,10 @@ def _build_middleware_stack(self, router_middlewares: List[Callable]) -> None:
# this must be the last middleware in the stack (tech debt for backward
# compatability purposes)
#
# This adapter will call the registered API using **kwargs only and not the middleware
# param list of get_response(app: ApiGatewayResolver, **kwargs)
# to ensure the call signature of existing defined routes of Users do not
# need to change (avoid breaking changes)
# This adapter will call the registered API passing only the expected route arguments extracted from the path
# and not the middleware.
# This adapter will adapt the response type of the route handler (Union[Dict, Tuple, Response])
# and normalise into a Resposne object so middleware will always have a constant signature
# and normalise into a Response object so middleware will always have a constant signature
all_middlewares.append(_registered_api_adapter)

# Wrap the original route handler function in the middleware handlers
Expand All @@ -341,7 +342,7 @@ def _build_middleware_stack(self, router_middlewares: List[Callable]) -> None:
#
# Start with the route function and wrap from last to the first Middleware handler.
for handler in reversed(all_middlewares):
self._call_stack = MiddlewareFrame(current_middleware=handler, next_middleware=self._call_stack)
self._middleware_stack = MiddlewareFrame(current_middleware=handler, next_middleware=self._middleware_stack)

self._middleware_stack_built = True

Expand Down Expand Up @@ -718,16 +719,14 @@ def __str__(self) -> str:
middleware_name = self.__name__
return f"[{middleware_name}] next call chain is {middleware_name} -> {self._next_middleware_name}"

def __call__(self, app: BaseRouter, **kwargs) -> Union[Dict, Tuple, Response]:
def __call__(self, app: BaseRouter) -> Union[Dict, Tuple, Response]:
"""
Call the middleware Frame to process the request.

Parameters
----------
app: BaseRouter
The router instance
**kwargs
Any additional arguments to pass to the middleware Frame

Returns
-------
Expand All @@ -742,17 +741,17 @@ def __call__(self, app: BaseRouter, **kwargs) -> Union[Dict, Tuple, Response]:
logger.debug("MiddlewareFrame: %s", self)
app._push_processed_stack_frame(str(self))

return self.current_middleware(app, self.next_middleware, **kwargs)
return self.current_middleware(app, self.next_middleware)


def _registered_api_adapter(
app: "ApiGatewayResolver",
get_response: Callable[..., Any],
**kwargs,
next_middleware: Callable[..., Any],
) -> Union[Dict, Tuple, Response]:
"""
Calls the registered API using ONLY the **kwargs provided to ensure the last call
in the chain will match the API route function signature.
Calls the registered API using the "_route_args" from the Resolver context to ensure the last call
in the chain will match the API route function signature and ensure that Powertools passes the API
route handler the expected arguments.

**IMPORTANT: This internal middleware ensures the actual API route is called with the correct call signature
and it MUST be the final frame in the middleware stack. This can only be removed when the API Route
Expand All @@ -762,20 +761,19 @@ def _registered_api_adapter(
----------
app: ApiGatewayResolver
The API Gateway resolver
get_response: Callable[..., Any]
next_middleware: Callable[..., Any]
The function to handle the API
**kwargs:
The arguments to pass to the API

Returns
-------
Response
The API Response Object

"""
logger.debug(f"Calling API Route Handler: {kwargs}")
route_args: Dict = app.context.get("_route_args", {})
logger.debug(f"Calling API Route Handler: {route_args}")

return app._to_response(get_response(**kwargs))
return app.to_response(next_middleware(**route_args))


class ApiGatewayResolver(BaseRouter):
Expand Down Expand Up @@ -1088,7 +1086,7 @@ def _call_route(self, route: Route, route_arguments: Dict[str, str]) -> Response
self._reset_processed_stack()

return ResponseBuilder(
self._to_response(
self.to_response(
route(router_middlewares=self._router_middlewares, app=self, route_arguments=route_arguments),
),
route,
Expand Down Expand Up @@ -1159,7 +1157,7 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> Optional[Resp

return None

def _to_response(self, result: Union[Dict, Tuple, Response]) -> Response:
def to_response(self, result: Union[Dict, Tuple, Response]) -> Response:
"""Convert the route's result to a Response

3 main result types are supported:
Expand Down Expand Up @@ -1330,6 +1328,6 @@ def __init__(


class NextMiddlewareCallback(Protocol):
def __call__(self, app: ApiGatewayResolver, **kwds: Any) -> Response:
"""Protocol for callback regardless of get_response(app, **kwargs), next(app, **kwargs)"""
def __call__(self, app: ApiGatewayResolver) -> Response:
"""Protocol for callback regardless of next_middleware(app), next(app)"""
...
26 changes: 11 additions & 15 deletions aws_lambda_powertools/event_handler/middlewares/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class BaseMiddlewareHandler(ABC):

This is the middleware handler function where middleware logic is implemented.
Here you have the option to execute code before and after the next handler in the
middleware chain is called. The next middleware handler is represented by `get_response`.
middleware chain is called. The next middleware handler is represented by `next_middleware`.


```python
Expand All @@ -20,15 +20,15 @@ class BaseMiddlewareHandler(ABC):
# or optionally raise an exception to short-circuit the middleware execution chain

# Get the response from the NEXT middleware handler (optionally injecting custom
# arguments into the get_response call)
result: Response = get_response(app, my_custom_arg="handled", **kwargs)
# arguments into the next_middleware call)
result: Response = next_middleware(app, my_custom_arg="handled")

# Place code here for actions AFTER the next middleware handler is called

return result
```

To implement ERROR style middleware wrap the call to `get_response` in a `try..except`
To implement ERROR style middleware wrap the call to `next_middleware` in a `try..except`
block - you can also catch specific types of errors this way so your middleware only handles
specific types of exceptions.

Expand All @@ -37,7 +37,7 @@ class BaseMiddlewareHandler(ABC):
```python

try:
result: Response = get_response(app, my_custom_arg="handled", **kwargs)
result: Response = next_middleware(app, my_custom_arg="handled")
except MyCustomValidationException as e:
# Make sure we send back a 400 response for any Custom Validation Exceptions.
result.status_code = 400
Expand All @@ -48,7 +48,7 @@ class BaseMiddlewareHandler(ABC):
```

To short-circuit the middleware execution chain you can either raise an exception to cause
the function call stack to unwind naturally OR you can simple not call the `get_response`
the function call stack to unwind naturally OR you can simple not call the `next_middleware`
handler to get the response from the next middleware handler in the chain.

for example:
Expand All @@ -63,12 +63,12 @@ class BaseMiddlewareHandler(ABC):


# Call the next middleware in the chain (needed for when condition above is valid)
return get_response(app, **kwargs)
return next_middleware(app)

"""

@abstractmethod
def handler(self, app: ApiGatewayResolver, get_response: NextMiddlewareCallback, **kwargs) -> Response:
def handler(self, app: ApiGatewayResolver, get_response: NextMiddlewareCallback) -> Response:
"""
The Middleware Handler

Expand All @@ -78,8 +78,6 @@ def handler(self, app: ApiGatewayResolver, get_response: NextMiddlewareCallback,
The ApiGatewayResolver object
get_response: NextMiddlewareCallback
The next middleware handler in the chain
kwargs: Any
Any additional arguments to pass to the next middleware handler

Returns
-------
Expand All @@ -93,22 +91,20 @@ def handler(self, app: ApiGatewayResolver, get_response: NextMiddlewareCallback,
def __name__(self) -> str: # noqa A003
return str(self.__class__.__name__)

def __call__(self, app: ApiGatewayResolver, get_response: Callable[..., Any], **kwargs) -> Response:
def __call__(self, app: ApiGatewayResolver, next_middleware: NextMiddlewareCallback) -> Response:
"""
The Middleware handler function.

Parameters
----------
app: ApiGatewayResolver
The ApiGatewayResolver object
get_response: Callable[...,Any]
next_middleware: NextMiddlewareCallback
The next middleware handler in the chain
kwargs:
Any additional arguments to pass to the next middleware handler

Returns
-------
Response
The response from the next middleware handler in the chain
"""
return self.handler(app, get_response, **kwargs)
return self.handler(app, next_middleware)
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def bad_config(self, error: InvalidSchemaFormatError) -> Response:
logger.debug(f"Invalid Schema Format: {error}")
raise InternalServerError("Internal Server Error")

def handler(self, app: ApiGatewayResolver, get_response: Callable[..., Any], **kwargs) -> Response:
def handler(self, app: ApiGatewayResolver, next_middleware: Callable[..., Any]) -> Response:
"""
Validate using Powertools validate() utility.

Expand All @@ -53,8 +53,7 @@ def handler(self, app: ApiGatewayResolver, get_response: Callable[..., Any], **k
Return the next middleware response if validation passes.

:param app: The ApiGatewayResolver instance
:param get_response: The original response
:param kwargs: Additional arguments
:param next_middleware: The original response
:return: The original response or HTTP 400 Response or HTTP 500 Response.

"""
Expand All @@ -66,7 +65,7 @@ def handler(self, app: ApiGatewayResolver, get_response: Callable[..., Any], **k
return self.bad_config(error)

# return next middleware response if validation passes.
result: Response = get_response(app, **kwargs)
result: Response = next_middleware(app)

if self.outbound_formats is not None:
try:
Expand Down
8 changes: 4 additions & 4 deletions examples/event_handler_rest/src/custom_middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
logger = Logger()


def validate_correlation_id(app: ApiGatewayResolver, get_response: NextMiddlewareCallback, **context_args) -> Response:
def validate_correlation_id(app: ApiGatewayResolver, next_middleware: NextMiddlewareCallback) -> Response:
# If missing mandatory header raise an error
if not app.current_event.headers.get("x-correlation-id", None):
raise BadRequestError("No [x-correlation-id] header provided. All requests must include this header.")

# Get the response from the next middleware and return it
return get_response(app, **context_args)
return next_middleware(app)


def sanitise_exceptions(app: ApiGatewayResolver, get_response: NextMiddlewareCallback, **context_args) -> Response:
def sanitise_exceptions(app: ApiGatewayResolver, next_middleware: NextMiddlewareCallback) -> Response:
try:
# Get the Result from the next middleware
result = get_response(app, **context_args)
result = next_middleware(app)
except Exception as err:
logger.exception(err)
# Raise a clean error for ALL unexpected exceptions (ServiceError based Exceptions are okay)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
logger = Logger()


def inject_correlation_id(app: APIGatewayRestResolver, get_response: NextMiddlewareCallback, **kwargs) -> Response:
def inject_correlation_id(app: APIGatewayRestResolver, next_middleware: NextMiddlewareCallback) -> Response:
request_id = app.current_event.request_context.request_id # (1)!

# Use API Gateway REST API request ID if caller didn't include a correlation ID
Expand All @@ -19,7 +19,7 @@ def inject_correlation_id(app: APIGatewayRestResolver, get_response: NextMiddlew
logger.set_correlation_id(request_id)

# Get response from next middleware OR /todos route
result = get_response(app, **kwargs) # (3)!
result = next_middleware(app) # (3)!

# Include Correlation ID in the response back to caller
result.headers["x-correlation-id"] = correlation_id # (4)!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
logger = Logger()


def log_request_response(app: APIGatewayRestResolver, get_response: NextMiddlewareCallback, **kwargs) -> Response:
def log_request_response(app: APIGatewayRestResolver, next_middleware: NextMiddlewareCallback, **kwargs) -> Response:
logger.info("Incoming request", path=app.current_event.path, request=app.current_event.raw_event)

result = get_response(app, **kwargs)
result = next_middleware(app)
logger.info("Response received", response=result.__dict__)

return result


def inject_correlation_id(app: APIGatewayRestResolver, get_response: NextMiddlewareCallback, **kwargs) -> Response:
def inject_correlation_id(app: APIGatewayRestResolver, next_middleware: NextMiddlewareCallback, **kwargs) -> Response:
request_id = app.current_event.request_context.request_id

# Use API Gateway REST API request ID if caller didn't include a correlation ID
Expand All @@ -25,17 +25,17 @@ def inject_correlation_id(app: APIGatewayRestResolver, get_response: NextMiddlew
logger.set_correlation_id(request_id)

# Get response from next middleware OR /todos route
result = get_response(app, **kwargs)
result = next_middleware(app)

# Include Correlation ID in the response back to caller
result.headers["x-correlation-id"] = correlation_id
return result


def enforce_correlation_id(app: APIGatewayRestResolver, get_response: NextMiddlewareCallback, **kwargs) -> Response:
def enforce_correlation_id(app: APIGatewayRestResolver, next_middleware: NextMiddlewareCallback) -> Response:
# If missing mandatory header raise an error
if not app.current_event.get_header_value("x-correlation-id", case_sensitive=False):
return Response(status_code=400, body="Correlation ID header is now mandatory.") # (1)!

# Get the response from the next middleware and return it
return get_response(app, **kwargs)
return next_middleware(app)
3 changes: 2 additions & 1 deletion tests/functional/event_handler/test_api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,8 @@ def test_debug_print_event(capsys):
# THEN print the event
out, err = capsys.readouterr()
assert "\n" in out
assert json.loads(out) == event
output: str = out.split("\n")[0]
assert json.loads(output) == event


def test_similar_dynamic_routes():
Expand Down
Loading