Fastapi: [QUESTION] How can I use Gunicorns SCRIPT_NAME with FastAPI?

Created on 3 Apr 2020  路  9Comments  路  Source: tiangolo/fastapi

First check

  • [x] I used the GitHub search to find a similar issue and didn't find it.
  • [x] I searched the FastAPI documentation, with the integrated search.
  • [x] I already searched in Google "How to X in FastAPI" and didn't find any information.

Description

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.

Additional context

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.

answered question

Most helpful comment

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,
    }

All 9 comments

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 URL localhost: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.

Was this page helpful?
0 / 5 - 0 ratings