Is it possible to allow routes to be prefixed using Gunicorn's support for the SCRIPT_NAME env variable? I read the discussion in #461 but could not find any other working solution than manually prepending the prefixed url to each route.
What I would like to do is basically have a route prefix by setting:
os.environ["SCRIPT_NAME"] = "/a/b/c/d/e"
and then allowing Gunicorn to route for example
@app.get("/hello")
def read_root():
return {"Hello": "World"}
to /a/b/c/d/e/hello.
I tried running the Uvicorn + Gunicorn combination using gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker to achieve this. It will simply ignore the prefix and keep on routing to /hello instead.
Gunicorn will do this out of the box if the env var is present when serving a Flask or Django app.
environ['SCRIPT_NAME'] isn't specifically a gunicorn variable, it is defined by WSGI as the variable indicating the root path of the application. FastAPI and uvicorn use ASGI, where this is replaced scope['root_path'] instead. I know you can use uvicorn --root-path="/a/b/c/d/e" when running uvicorn directly via command-line, though I'm not familiar with the worker process way of using it but I'm not seeing it set root_path in its config_kwargs dict, so that might be a uvicorn bug that you should raise on their end.
Be sure to test if uvicorn --root-path="/a/b/c/d/e" actually does what you want, though. I know SCRIPT_NAME and root_path are supposed to be equivalent because the ASGI spec says so, but I'm not aware of either normally affecting the routing in the way you're describing.
@magdapoppins the solution is actually in my demo project linked in #461 on the root-path branch. Basically, you have to subclass the UvicornWorker in order to configure it.
I've tried to set uvicorn --root-path="api/v1" myapp.main:app. I would expect that I now can call the FastAPI endpoint on URL localhost:8000/api/v1/hello, but I get this response: {"detail": "Not Found"}. When I check the logs, it looks like the root path has only changed internally:
?[32mINFO?[0m: Started server process [?[36m98168?[0m]
?[32mINFO?[0m: Waiting for application startup.
?[32mINFO?[0m: Application startup complete.
?[32mINFO?[0m: Uvicorn running on ?[1mhttp://127.0.0.1:8000?[0m (Press CTRL+C to quit)
?[32mINFO?[0m: 127.0.0.1:52328 - "POST api/v1/api/v1/hello HTTP/1.1" 404
So the request still only works on URL localhost:8000/hello, but I would like it to work on URL localhost:8000/api/v1/hello.
I'm using Python 3.7.6, FastAPI 0.52.0 & uvicorn 0.11.3.
Thanks for any hints on this!
@kevroes I'm not a maintainer here but that looks like a separate issue to me. What you are missing is a proxy. You can also look at my demo project for an Nginx configuration that sets it up properly.
@kevroes
So the request still only works on URL
localhost:8000/hello, but I would like it to work on URLlocalhost:8000/api/v1/hello.
--root_path is meant to indicate the path at which you'll be serving your application, so it's useful if you use a proxy of some sort to mount localhost:8000 to http://192.168.1.100/mysite so that localhost:8000/hello becomes localhost:8000/mysite/hello. In that case, you would use --root-path="/mysite".
What you probably want to do is either have an APIRouter for /api/v1 mounted onto your base app, like this:
app = FastAPI()
apiv1_router = APIRouter()
@apiv1_router.get('/hello')
def hello_v1():
pass
app.include_router(apiv1_router, prefix="/api/v1")
...or to have each API endpoint be its own separate FastAPI app (which I suspect is what you want, given the api/v1 prefix) to keep the namespaces separate. For that, you would do
base_app = FastAPI()
apiv1_app = FastAPI()
base_app.mount(apiv1_app, prefix="/api/v1")
Thanks for the discussion here everyone! So, yeah, root_path would be the ASGI equivalent of SCRIPT_NAME in WSGI.
It was not supported by FastAPI until recently but it is now from version 0.56.0: https://fastapi.tiangolo.com/advanced/behind-a-proxy/ :tada:
Now, about using Uvicorn with Gunicorn, I'm not sure if Gunicorn passes SCRIPT_NAME to Uvicorn and if Uvicorn reads it to pass the root_path, I think currently it doesn't. But I haven't checked thoroughly.
I think that's worth implementing in Uvicorn. But at least on the FastAPI side, it should now work :smile:
As shown in my proof-of-concept, you still have to subclass the uvicorn worker similar to the following:
from starlette.config import Config
from uvicorn.workers import UvicornWorker
config = Config()
class ConfigurableWorker(UvicornWorker):
"""
Define a UvicornWorker that can be configured by modifying its class attribute.
All of the command line options for uvicorn are potential configuration options
(see https://www.uvicorn.org/settings/ for the complete list).
"""
#: dict: Set the equivalent of uvicorn command line options as keys.
CONFIG_KWARGS = {
"root_path": config("SCRIPT_NAME", default=""),
"proxy_headers": True,
}
@Midnighter I think that's worth a PR to Uvicorn :heavy_check_mark:
Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.
Most helpful comment
As shown in my proof-of-concept, you still have to subclass the uvicorn worker similar to the following: