-
Notifications
You must be signed in to change notification settings - Fork 405
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
feat(event_handler): add Middleware support for REST Event Handler #2917
feat(event_handler): add Middleware support for REST Event Handler #2917
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is so exciting!! I've raised a few questions on areas that weren't immediately clear to me, tiny improvements, and one around implementing a short-circuiting for middlewares to return early.
I'll review tests later. Thank you so much for taking this on @walmsles
Question: I am thinking of relocating the internal registered_api_adapter into the api_gateway.py module and enforce typing of middleware to be inherited from the BaseMiddlewareHandler - I feel enforcing class usage for middleware allows for easier construction and configuration of middleware handlers. Much simpler to construct and reason about IMO. |
go for it!
|
@heitorlessa have improved the BaseMiddlewareHandler class and implemented SchemaValidationMiddleware (based on the existing Powertools validate function). Enforcing Class based middleware is not possible due to the cyclic nature of api_gateway.py and middleware dependencies - Leaving it as "Callable" will suit a wider audience of devs and is required for the registered_api_adapter to work. Let me know of any updates required - The code is done and now the hard part - documentation |
Thanks a lot!! I'll look into the changes tomorrow :) |
Codecov ReportPatch coverage:
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## develop #2917 +/- ##
===========================================
- Coverage 96.51% 96.36% -0.16%
===========================================
Files 180 184 +4
Lines 7909 8050 +141
Branches 1491 1506 +15
===========================================
+ Hits 7633 7757 +124
- Misses 221 236 +15
- Partials 55 57 +2
☔ View full report in Codecov by Sentry. 📢 Have feedback on the report? Share it here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks a lot for those changes! Managed to spend more quality time to debug and understand this now - made some initial asks to start to be ready for prod.
Adding tasks so we get this to the finish line - we can divide and conquer (let me know the ones you want to pick up)
- Change docstrings to Numpy (our style)
- Add
logger.debug
statements in key areas to improve observability - Include current middleware name and next in line in the call chain (suggested)
- Include debugging for anyone printing
get_response
to find out the call chain - Address comments and/or suggestions
- Add missing tests for middlewares with
Router
- Add missing tests for middlewares short circuiting via HTTP Errors
- Improve early return test by having a third middleware to ensure it doesn't get called
- Make attributes private unless we need to expose them (if so, docstrings)
- Update docstrings with examples where relevant
- Expand docstring for
registered_api_adapter
to provide an example on breaking change (hard to get ull picture if glancing through without debugging) - Update docstring on how middlewares stack is built with an ascii diagram (who takes precedence, etc. - monodraw might be the best tool)
- Consider enriching exceptions with registered middlewares to ease debugging (which middlewares were registered? what was the call chain?) -- best effort
- Create diagram for happy path - Middleware at route level and router level (use()) (@walmsles WIP)
- Create diagram for happy path - Middleware defined in both main and split routes (@walmsles WIP)
- Create diagram for early return - short circuiting (@walmsles WIP)
- Document patterns for authoring middlewares - being a good citizen
- Document anti-patterns (e.g., avoid discarding responses in first middleware unless intentional)
- Document design in alignment with Chalice
Let's tick off some of these items :-).
@heitorlessa lets discuss enriching exceptions techniques to assist with best-effort debugging |
@heitorlessa last round of commits added:
Still to do:
|
Quick notes through peer reviewing:
|
Renamed to
Removed - less code is better code!
@heitorlessa - please think about including processed stack frames into he debug exception response - I have struggled with getting stack trace into JSON formattable string to get something close to: {
"body": {
"stack_trace": ".... stack trace",
"middleware_stack": [
"Middleware 'SchemaValidationMiddleware'. Next call chain is: SchemaValidationMiddleware -> NextMiddleware"
]
}
} But it is just not working |
I'm planning to use this middleware to build automatic Pydantic validation based on route and the function params. In order for this to work, I need to have access to the Route that was matched during my middleware. Do you know if there's a way to do it the way this is implemented, or can we consider explosing that to the Middlewares? |
Hi @rubenfonseca - the only state passed between middleware is the Resolver (Router) object itself so middleware has access to the "app" instance created by the developer. The actual "Route" that is being resolved is not currently referenced by the Resolver (which does seem unusual). I feel the best way to achieve this would be to add a reference to the resolved Route object to the additional context - which is already managed between invocations and feels like the right place for this type of reference since it is Context related. Have pushed a change adding "powertools_route" to the additional context - let me know your thoughts. |
Looking at the docs <3 |
…re for Cors and Compression as examples
Signed-off-by: heitorlessa <[email protected]>
Two tests for the middleware order to ensure we don't introduce breaking change. I'll call out that in the docs now so a customer can decide whether to use the default or reverse order. Appending to context logic is the same to ensure we are only testing the order in which @pytest.mark.parametrize(
"app, event",
[
(ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT),
(APIGatewayRestResolver(), API_REST_EVENT),
(APIGatewayHttpResolver(), API_RESTV2_EVENT),
],
)
def test_api_gateway_middleware_order_with_include_router_last(app: EventHandlerInstance, event):
# GIVEN two global middlewares: one for App and one for Router
router = Router()
def global_app_middleware(app: EventHandlerInstance, next_middleware: NextMiddleware):
middleware_order: List[str] = router.context.get("middleware_order", [])
middleware_order.append("app")
app.append_context(middleware_order=middleware_order)
return next_middleware(app)
def global_router_middleware(router: EventHandlerInstance, next_middleware: NextMiddleware):
middleware_order: List[str] = router.context.get("middleware_order", [])
middleware_order.append("router")
router.append_context(middleware_order=middleware_order)
return next_middleware(app)
@router.get("/my/path")
def dummy_route():
middleware_order = app.context["middleware_order"]
assert middleware_order[0] == "app"
assert middleware_order[1] == "router"
return Response(status_code=200, body="works!")
# WHEN App global middlewares are registered first
# followed by include_router
router.use([global_router_middleware]) # mimics App importing Router
app.include_router(router)
app.use([global_app_middleware])
# THEN resolving a request should start processing global Router middlewares first
# due to insertion order
result = app(event, {})
assert result["statusCode"] == 200 @pytest.mark.parametrize(
"app, event",
[
(ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent), API_REST_EVENT),
(APIGatewayRestResolver(), API_REST_EVENT),
(APIGatewayHttpResolver(), API_RESTV2_EVENT),
],
)
def test_api_gateway_middleware_order_with_include_router_first(app: EventHandlerInstance, event):
# GIVEN two global middlewares: one for App and one for Router
router = Router()
def global_app_middleware(app: EventHandlerInstance, next_middleware: NextMiddleware):
middleware_order: List[str] = router.context.get("middleware_order", [])
middleware_order.append("app")
app.append_context(middleware_order=middleware_order)
return next_middleware(app)
def global_router_middleware(router: EventHandlerInstance, next_middleware: NextMiddleware):
middleware_order: List[str] = router.context.get("middleware_order", [])
middleware_order.append("router")
router.append_context(middleware_order=middleware_order)
return next_middleware(app)
@router.get("/my/path")
def dummy_route():
middleware_order = app.context["middleware_order"]
assert middleware_order[0] == "router"
assert middleware_order[1] == "app"
return Response(status_code=200, body="works!")
# WHEN App include router middlewares first
# followed by App global middlewares registration
router.use([global_router_middleware]) # mimics App importing Router
app.include_router(router)
app.use([global_app_middleware])
# THEN resolving a request should start processing global Router middlewares first
# due to insertion order
result = app(event, {})
assert result["statusCode"] == 200 |
👌
It is important to make it clear that the merging of global middleware happens when For clarity, it may be easier to:
Is the above simpler to reason about? The current implementation does the merging when router.use([global_router_middleware]) # mimics App importing Router
app.use([global_app_middleware])
app.include_router(router)
app.use([global_app_middleware_two]) You end up with the middleware order: global_app_middleware, global_router_middleware, global_app_middleware_two The alternative I outlined above would result in the following order: global_app_middleware, global_app_middleware_two, global_router_middleware It is simpler to reason about but also out of order regarding the process of constructing the app routes - there are definite pros/cons for each. |
Signed-off-by: heitorlessa <[email protected]>
Signed-off-by: heitorlessa <[email protected]>
Signed-off-by: heitorlessa <[email protected]>
Merging after CI checks complete 🎉 Finally over @walmsles -- THANK YOU so much for all the time you've put into this to make it a reality. I've cleaned up the last bits, added mentions of Middleware for Router, and am leaving this to another PR so I can get started on re:Invent stuff.
|
Signed-off-by: heitorlessa <[email protected]>
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there, @heitorlessa and @walmsles! I must say, it was a blast watching this PR come together and seeing how you tackled all those decisions about code and UX best practices. Seriously impressive work!
This is now released under 2.24.0 version! |
Issue number: #953 #1955 #1696
Summary
Adds middleware support to event_handlers for REST API Gateway resolvers (all types). First pass POC implementation following a similar implementation style to Chalice and Starlette
Changes
middleware
parameter expecting a list ofCallable
middleware handlersregistered_api_middleware
, which is added as the last middleware handler internally (maintain backward compatibility) - without this function, the signature for REST routes would change to match the middleware signature, which feels like a breaking change. This special middleware also ensures all middleware will get a Response object as the response, which is essential for being able to write middleware (no type guessing).Callable
User experience
Users can now add middleware either for a specific route or centrally for ALL routes:
Technical Debt
The following elements are listed as required technical debt to avoid breaking changes for existing REST API resolver REST users.
registered_api_adapter
- this middleware adapter will ensure the existing API route functions will be called in the same way they are defined today, and it MUST be the last middleware handler in the Middleware call stack - LOTS of comments and context around where this happens when building the middleware call stack.registered_api_adapter
inaws_lambda_powertools/event_handler/middlewares/registered_api.py
, but also wanted to place the middleware handler in the middleware folder where it belongs - could relocate this into api_gateway.py module (since is internal to powertools).MiddlewareStackWrapper.__call__
isUnion[Dict, Tuple, Response]
. Ideally this would be consistent as Response - but not possible since API routes can return one of those 3 types - unsure if that could ever change but feel worth calling it out.Checklist
If your change doesn't seem to apply, please leave them unchecked.
Is this a breaking change?
No, not a breaking change (I hope)
Checklist:
Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.