gradio-app / gradio

Build and share delightful machine learning apps, all in Python. 🌟 Star to support our work!
http://www.gradio.app
Apache License 2.0
34.13k stars 2.6k forks source link

Gradio FastAPI with root_path middleware has path resolution problems #9529

Open aronatkins opened 1 month ago

aronatkins commented 1 month ago

Describe the bug

This problem likely overlaps with https://github.com/gradio-app/gradio/issues/8073 and https://github.com/gradio-app/gradio/issues/9101. The approach (using middleware) is different, which is why I'm filing it separately.

The application is hosted at http://localhost:4080/gradio, but sees requests for http://localhost:4080/assets/index-BOW2xVAS.css and http://localhost:4080/theme.css.

Have you searched existing issues? 🔎

Reproduction

This Python script has several attempts to mount a Gradio application beneath a known root path. The two middleware approaches model our situation, as we determine the root_path based on incoming request information (an HTTP header). The middleware here is simplified and uses a static root_path, but the idea is the same.

The RootPathMiddleware and AllPathMiddleware classes are similar; they are experiments that adjust only root_path or all of root_path, path, and raw_path.

import gradio as gr

def greet(name):
    return "Hello, " + name + "!"

with gr.Blocks() as demo:
    gr.Markdown("Welcome to your Gradio application!")

    name = gr.Textbox(label="Name", elem_id="name")
    greeting = gr.Textbox(label="Greeting", elem_id="greeting")
    button = gr.Button("Greet", elem_id="greet")
    button.click(fn=greet,
                 inputs=name,
                 outputs=greeting,
                 api_name="greet")

# Middleware that injects a /gradio root_path.
class RootPathMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send) -> None:
        if scope["type"] == "http":
            # print(f"RootPathMiddleware(in): {scope}", flush=True)
            scope["root_path"] = "/gradio"
            # print(f"RootPathMiddleware(out): {scope}", flush=True)
        await self.app(scope, receive, send)

def join(a, b):
    return a.rstrip("/") + "/" + b.lstrip("/")

# Middleware that injects a /gradio root_path and ensures that path and
# raw_path are prefixed by that root.
class AllPathMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send) -> None:
        if scope["type"] == "http":
            # print(f"AllPathMiddleware(in): {scope}", flush=True)
            scope["root_path"] = "/gradio"
            scope["path"] = join(scope["root_path"], scope["path"])
            scope["raw_path"] = join(scope["root_path"], scope["raw_path"].decode()).encode()
            # print(f"AllPathMiddleware(out): {scope}", flush=True)
        await self.app(scope, receive, send)

if __name__ == '__main__':
    import uvicorn

    host = "0.0.0.0"
    port = 12345

    #
    # Direct launch with root_path.
    # CSS preload error assets/index-*.css (#9101)
    # UI appears OK.
    #
    # demo.launch(
    #     server_name=host,
    #     server_port=port,
    #     root_path='/gradio',
    # )

    #
    # Launch a FastAPI app with root_path.
    # CSS preload error assets/index-*.css (#9101)
    # CSS preload error theme.css; UI unstyled (#8073)
    #
    # app = gr.routes.App.create_app(demo, app_kwargs={"root_path": '/gradio'})
    # uvicorn.run(app, host=host, port=port)

    #
    # Launch a FastAPI app using uvicorn root_path.
    # CSS preload error assets/index-*.css (#9101)
    # CSS preload error theme.css; UI unstyled (#8073)
    #
    # app = gr.routes.App.create_app(demo)
    # uvicorn.run(app, host=host, port=port, root_path='/gradio')

    #
    # Launch a FastAPI with root path and path along with uvicorn root path.
    # CSS preload error assets/index-*.css (#9101)
    # CSS preload error theme.css; UI unstyled (#8073)
    # Similar to suggestion in https://github.com/gradio-app/gradio/issues/8986
    #
    # app = gr.routes.App.create_app(demo, app_kwargs={"root_path": '/gradio', "path": "/"})
    # uvicorn.run(app, host=host, port=port, root_path="/gradio")

    #
    # Launch a FastAPI app with root_path middleware.
    # CSS preload error assets/index-*.css (#9101)
    # CSS preload error theme.css; UI unstyled (#8073)
    #
    app = gr.routes.App.create_app(demo)
    app = RootPathMiddleware(app)
    uvicorn.run(app, host=host, port=port)

    #
    # Launch a FastAPI app with root_path, path, raw_path middleware.
    # CSS preload error assets/index-*.css (#9101)
    # CSS preload error theme.css; UI unstyled (#8073)
    #
    # app = gr.routes.App.create_app(demo)
    # app = AllPathMiddleware(app)
    # uvicorn.run(app, host=host, port=port)

This is the nginx configuration used to test against this application. It includes an App-Base-URL header, but it is not used by the Python code.

events {}

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ""      close;
    }

    server {
        listen 80;

        location /gradio {
            proxy_pass                 http://gradio:12345/;

            rewrite                    ^/gradio/(.*)$ /$1 break;
            proxy_redirect             / /gradio/;

            proxy_pass_request_headers on;

            proxy_buffering            off;
            proxy_cache                off;
            chunked_transfer_encoding  on;

            proxy_set_header           Upgrade $http_upgrade;
            proxy_set_header           Connection $connection_upgrade;

            proxy_set_header           App-Base-URL /gradio;

            # https://www.gradio.app/guides/running-gradio-on-your-web-server-with-nginx
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $http_host;
            proxy_set_header X-Forwarded-Proto $scheme;

        }
    }
}

The docker-compose.yml:

services:
  gradio:
    image: gradio
    build: .
    ports:
      - "4001:12345"

  nginx:
    image: nginx:1.21
    ports:
      - "4080:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./index.html:/etc/nginx/html/index.html
    depends_on:
      - gradio

The Dockerfile containing gradio and uvicorn:

FROM ubuntu:22.04

RUN apt-get update -qq && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y \
    apt-transport-https \
    curl \
    libcurl4-openssl-dev \
    libev-dev \
    locales \
    python3 \
    python3-pip \
    tzdata && \
    rm -rf /var/lib/apt/lists/*

RUN PIP_ROOT_USER_ACTION=ignore pip3 install gradio uvicorn

RUN useradd -r -m -s /usr/sbin/nologin gradio

COPY app.py /content/

EXPOSE 12345

USER gradio

ENTRYPOINT [ "/usr/bin/python3", "/content/app.py" ]

Screenshot

These screenshots were taken when using the RootPathMiddleware middleware, but other runtime options produced similar results.

The unstyled application:

image

The 404 errors:

image

Logs

No response

System Info

The output of `gradio environment` taken from within the Docker image defined above:

Gradio Environment Information:
------------------------------
Operating System: Linux
gradio version: 4.44.1
gradio_client version: 1.3.0

------------------------------------------------
gradio dependencies in your environment:

aiofiles: 23.2.1
anyio: 4.6.0
fastapi: 0.115.0
ffmpy: 0.4.0
gradio-client==1.3.0 is not installed.
httpx: 0.27.2
huggingface-hub: 0.25.1
importlib-resources: 6.4.5
jinja2: 3.1.4
markupsafe: 2.1.5
matplotlib: 3.9.2
numpy: 2.1.1
orjson: 3.10.7
packaging: 24.1
pandas: 2.2.3
pillow: 10.4.0
pydantic: 2.9.2
pydub: 0.25.1
python-multipart: 0.0.12
pyyaml: 6.0.2
ruff: 0.6.9
semantic-version: 2.10.0
tomlkit==0.12.0 is not installed.
typer: 0.12.5
typing-extensions: 4.12.2
urllib3: 2.2.3
uvicorn: 0.31.0
authlib; extra == 'oauth' is not installed.
itsdangerous; extra == 'oauth' is not installed.

gradio_client dependencies in your environment:

fsspec: 2024.9.0
httpx: 0.27.2
huggingface-hub: 0.25.1
packaging: 24.1
typing-extensions: 4.12.2
websockets: 12.0


### Severity

Blocking usage of gradio
aronatkins commented 1 month ago

Rolling back to older versions of (all the things) avoids the problems described here. This isn't a reasonable workaround, for sure, but does capture that there has been change to the treatment of application paths.

Using the following package installation (modifying the included Dockerfile) results in a Gradio application that does not show errors:

RUN PIP_ROOT_USER_ACTION=ignore pip3 install gradio==3.3.1 fastapi==0.85.2 httpx==0.24.1 uvicorn