-
-
Notifications
You must be signed in to change notification settings - Fork 128
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
Allow custom path and root_path resolution logic in ASGI scope #147
Comments
@chesnutcase This makes a lot of sense, thanks for the detailed issue description. 👍 Support for custom resolution logic is something that should probably be included here, though I'm not certain yet how it should be designed. I've generally wanted to avoid subclassing the adapter itself, but perhaps there isn't a better alternative in this case (have to think a bit more on it). If we did end up going with your suggested solution, then I think a single Do you have an idea of how usage of the subclassed approach might look? |
Hi. Firstly a thumbs up that I am finding the same issue here with FastAPI deployment on Lambda and then putting that behind the AWS API Gateway. All works fine with Mangum when using custom domain without a path proxy, but when using a custom path proxy, (e.g. /custom) the application gets confused and returns 404s. I can fix it by adding a prefix on all paths in the app, but as @chesnutcase says, it makes it not very portable. As reported here for a similar case (CodeGenieApp/serverless-express#216) it looks as though the path passed to the FastAPI app by the Lambda event is the full path including the base path. Looking at the codebase I've just noticed the If that works, then could this parameter be set automatically from the Just a half-baked idea. I'm not an expert on your package, but very much support the suggestion for better portability. |
An update. I tested that and it works well as a workaround
This all worked nicely. The issue raised by @chesnutcase on portability still applies and it would be nice if we could get the PROXY_PATH from the Lambda events somehow, and also push it into the FastAPI root_path |
I see the merged PR #162 That looks good. Will that then resolve this issue? I've been through the source code and it looks as though it might but I wanted to check as we probably want to update docs somewhere for the API Gateway / Mangum / FastAPI use case which is what is affected here. The workaround I use for now that works is to hardcode in the I am assuming we can then remove both after you have released this update. But I haven't got my head around how the Am I missing something and more is needed? |
I found a workaround that does enable the ROOT_PATH to be set dynamically for the STAGED endpoints at least. Custom domains require a little more work. Once the @app.middleware("http")
async def set_root_path_for_api_gateway(request: Request, call_next):
"""Sets the FastAPI root_path dynamically from the ASGI request data."""
root_path = request.scope["root_path"]
if root_path:
app.root_path = root_path
response = await call_next(request)
return response As it happens, Mangum doesn't set the @app.middleware("http")
async def set_root_path_for_api_gateway(request: Request, call_next):
"""Sets the FastAPI root_path dynamically from the ASGI request data."""
root_path = request.scope["root_path"]
if root_path:
# Assume set correctly in this case
app.root_path = root_path
else:
# fetch from AWS requestContext
if "aws.event" in request.scope:
context = request.scope["aws.event"]["requestContext"]
if "customDomain" not in context:
# Only works for stage deployments currently
root_path = f"/{context['stage']}"
if request.scope["path"].startswith(root_path):
request.scope["path"] = request.scope["path"][len(root_path) :]
request.scope["root_path"] = root_path
app.root_path = root_path
# NOT IMPLEMENTED FOR customDomain
# root_path = f"/{context['customDomain']['basePathMatched']}"
response = await call_next(request)
return response If we could get |
@ttamg how configurable would a solution here need to be? Could your middleware solution (or something similar) be used to accurately determine the correct scope keys in all cases without additional configuration/customisation? If we can modify the adapter to handle this using a common method that is able figure out the correct path variables (and deprecate the Happy to work with you reach a resolution on this, but I need more information - if you have an idea for a draft PR I'd be glad to review it. |
@jordaneremieff we are getting beyond my pay grade here. My thinking was that at least on the FastAPI side, if we set the But that means mangum would need to parse the I had a play with it myself a few days ago to see if it was consistent what is added in the So in summary I think it is very doable, but I worry it will be brittle unless we get someone who knows what's stable at AWS API Gateway to let us know the |
Hey all, sorry I disappeared from the discussion. Admittedly, I haven't touched FastAPI&API Gateway in a few months so I need some time to get back to speed with this issue. The main point I had in mind when I opened this issue was to simply allow the user to customize the root path resolution logic (i.e. allowing the user to pass a string doesn't count). However, there are so many ways a HTTP application in a Lambda function can be integrated into API gateway depending on the user's needs. We can try to have a sensible default that uses heuristics to "guess" what the correct root path is. But just in case, we should leave it extensible by the user (probably by extending the Mangum class and overriding a method). I too find the AWS documentation very lacking when it comes to explaining what each field in the context variable does in different scenarios (especially in the case of custom domain names with path mappings). Maybe to work on the heuristic we can first draw up a table of all the use cases we want to support, then do experiments in each case to discuss what strategies we have for evaluating the correct scope in each case.
Summary of use cases: Custom Domain NameCustom Domain with base path mapping that maps to API Id, Stage Name, Path (optional). If we can somehow inspect the base path mapping from the integration event or context this should be trivial. Single Application in StageFor the "hello world" applications that do not use a domain name and the proxy resource with the lambda proxy integration is located at the root of the stage. Multiple Application in StageFor more complex applications that do not use custom domain names (or private API inside a VPC) where the proxy resource with the lambda proxy integration is not located at the root of the stage, also possible that the API Gateway hosts different proxy resources pointing to different lambdas (what I originally outlined in the first comment). |
I have not extensively tested this approach but this seems to work if you are proxying with stage link (ie, /prod) and custom domain mapped to a path. @app.middleware("http")
async def set_root_path_for_api_gateway(request: Request, call_next):
print(f"request.scope : {request.scope['root_path']}")
"""Sets the FastAPI root_path dynamically from the ASGI request data."""
root_path = request.scope["root_path"]
print(f"root_path : {root_path}")
if root_path:
# Assume set correctly in this case
app.root_path = root_path
else:
# fetch from AWS requestContext
if "aws.event" in request.scope:
context = request.scope["aws.event"]["requestContext"]
if "pathParameters" in request.scope["aws.event"]:
if request.scope['aws.event']['pathParameters'] is not None and 'proxy' in request.scope['aws.event']['pathParameters']:
request.scope["path"] = f"/{request.scope['aws.event']['pathParameters']['proxy']}"
path_parameters = request.scope["aws.event"]["pathParameters"]
root_path = context['path'] [ : context['path'].find(path_parameters["proxy"]) ]
request.scope["root_path"] = root_path
response = await call_next(request)
return response |
@bafonso Thank you so much for this breadcrumb. This helped me fix an issue I have been fighting all day. It should be noted though that the code provided only seems to work with API Gateway payload 1.0! I ended up using the same approach but modified it to work with payload 2.0. The issue I was running into was that I am using stages in APIGateway to deploy multiple versions of an API. We can use the If it's anything else we know that our This is not heavily tested by any means -- so use at your own discretion. It has at lease unblocked me for now. Hopefully it helps someone else! class AWSAPIGatewayMiddleware(BaseHTTPMiddleware):
"""
Handles the FastAPI path and root_path dynamically from the ASGI request data.
Mangum injects the AWS event data which we can use to dynamically set the path
and root_path.
https://github.com/jordaneremieff/mangum/issues/147
"""
def __init__(self, app: ASGIApp) -> None:
"""Initialize the middleware"""
super().__init__(app)
self.app = app
async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
"""Process the request and call the next middleware"""
root_path = request.scope["root_path"]
if root_path:
# Assume set correctly in this case
self.app.root_path = root_path # type: ignore
else:
stage = request.scope["aws.event"]["requestContext"]["stage"]
# Check if stage is the default, if so, we don't need to do anything
if stage != "$default":
# If stage is not $default, it means we are behind an APIGateway
# stage and we need to set the path and root_path values correctly
# For example if the stage is "dev", and the path is "/dev/users/123"
# the root_path should be "/dev" and the path should be "/users/123"
# AWS/APIGateway conveniently provides pathParameters.proxy
# which is the path after the stage_part. We can use this to
# set the path.
# Set root_path value to APIGateway stage in requestContext
stage_path = f"/{stage}"
self.app.root_path = stage_path
request.scope["root_path"] = stage_path
# Set path value to proxy path from event
request.scope[
"path"
] = f"/{request.scope['aws.event']['pathParameters']['proxy']}"
response = await call_next(request)
return response |
@rernst76 A handler pattern and The original suggested solution was to modify the main adapter class, but handlers are now responsible for the scope resolution details and would be the equivalent solution at this point. You can have a look at the APIGateway handler and example of a custom handler in here. |
Hi, haven't read through all the discussion, but I was able to come up with a quite neat solution based on Jordan's comment above. My issue was specifically that I wanted FastAPI to dynamically set the I found I could infer the root path by comparing the The custom handler looks like this: from typing import Any
from mangum import Mangum
from mangum.handlers import APIGateway
from mangum.types import Scope, LambdaEvent, LambdaContext, LambdaConfig
from .main import app
def find_root_path(event: LambdaEvent) -> str:
# This is the full path, including /<stage> at the start
request_path = event.get("requestContext", {}).get("path", "")
# This is the path of the resource, not including a prefix
resource_path = event.get("path", "")
root_path = ""
if request_path.endswith(resource_path):
root_path = request_path[: -len(resource_path)]
return root_path
class APIGatewayCorrectedRootPath(APIGateway):
"""A variant of the APIGateway Mangum handler which guesses the root path.
The `root_path` property of the ASGI scope is intended to indicate a
subpath the API is served from. This handler will try to guess this
prefix based on the difference between the requested path and the
resource path API gateway reports.
Using this should eleviate the need to manually specify the root path in
FastAPI.
"""
def __init__(
self,
event: LambdaEvent,
context: LambdaContext,
config: LambdaConfig,
*_args: Any
) -> None:
super().__init__(event, context, config)
@property
def scope(self) -> Scope:
return {**super().scope, "root_path": find_root_path(self.event)}
handler = Mangum(app, custom_handlers=[APIGatewayCorrectedRootPath]) @jordaneremieff, would you be interested in merging a PR adding this functionality as an opt-in to EDIT: I originally posted with with a bogus suggestion for how it could be implemented, which I realized wouldn't work. I think it can be added as a separate handler, exactly like I do above, or by adding a new handler = Mangum(app, apigw_infer_root=True) |
The `api_gateway_infer_root_path` option instructs Mangum to infer the `root_path` ASGI scope property based on the AWS API Gateway event object. This enables applications to know what subpath they are being served from, without explicit configuration. Relates to Kludex#147.
FastAPI + mangum works well for single-backend API gateways, but for API gateways where different resources can point to different FastAPI applications in different stacks (a true "gateway") this can break pretty quickly. I think the logic for
path
androot_path
variables in the ASGI scope object should be extensible by the user – right now,root_path
is hardcoded to a blank string (which I do not think is correct according to the ASGI specification).Consider an example:
For the FastAPI (mangum) applications to work in this setup,
api_gateway_base_path
in the handlers of each application must be hardcoded to exactly match/service_a
and/service_b
respectively (the resource names in API Gateway).I do not think that this is a good practice, because this means that the lambda code needs to be aware of the value of the settings used in API Gateway (coupling?). This hurts the portability and "plug and play" functionality of API Gateway Integrations with lambda applications. It also means I am unable to use the same codebase to mount two separate copies of a service under two different service names within the same API gateway.
The good news is that theoretically speaking the application is able to infer what is the true root path of the application (either
/prod
or/prod/service_a
or even just/service_a
in the case of custom domain names) just through the event that API gateway passes (therequestContext
key contains information such as stage name, domain name and resource name). The AWS documentation also recommends the use of stage variables (that are also passed to the proxy event object) for application configuration:I believe this concept can be extended to the root path resolution as well. Through the use of stage variables, the API gateway decides where the FastAPI application should be mounted; the application simply reads this information at invocation time.
Suggested Solution
Instead of hardcoding a blank string in
root_path
and strippingpath
usingapi_gateway_base_path
, simply define methods that can be overridden by child classes:Inside
adapter.py
,Mangum.__call__
:Mangum.resolve_asgi_path
and the others can contain the default implementation of strippingpath
ofapi_gateway_base_path
for backward compatibility, but the point here is to allow other clients to customise the resolution logic depending on their use case (single backend api gateway? multiple backend api gateway? with or without custom domains?) by extending the class and overriding these methods.The text was updated successfully, but these errors were encountered: