Repository: gradio-app/gradio

Issue:
get_api_call_path() fails when x-forwarded-host set
### Describe the bug

`x-forwarded-host` contains only host, e.g. `example.com` so the function `get_request_url()` returns only protocol + host but without a path. This causes `get_api_call_path()` fails with `ValueError(f"Request url '{root_url}' has an unkown api call pattern.")` if `x-forwarded-host` is set.

I mentioned it here in the review: [https://github.com/gradio-app/gradio/pull/10841](https://github.com/gradio-app/gradio/pull/10841)

### Have you searched existing issues?  🔎

- [x] I have searched and found no existing issues

### Reproduction

Server:
```python
import gradio as gr


# Define a simple function to be used in the Gradio interface
def greet(name):
    return f"Hello, {name}!"


# Create a Gradio interface
interface = gr.Interface(
    fn=greet,  # Function to call
    inputs=gr.Textbox(label="Enter your name"),  # Input component
    outputs=gr.Textbox(label="Greeting"),  # Output component
)

# Launch the interface
if __name__ == "__main__":
    interface.launch()


```

Client:
```python
from gradio_client import Client

client = Client("http://localhost:7860/")
result = client.predict(name="Test", api_name="/predict")
print(result)

# This causes exception on the server
client = Client("http://localhost:7860/", headers={"X-Forwarded-Host": "localhost:7860"})
result = client.predict(name="Test", api_name="/predict")
print(result)
```


### Screenshot

_No response_

### Logs

```shell

```

### System Info

```shell
Gradio 5.23.0
```

### Severity

Blocking usage of gradio

Discussion:
@abidlabs: I think this is fixed in 5.23.1, right?@matrn: > I think this is fixed in 5.23.1, right?

You are right, it works in 5.23.1.

But shouldn't be the function `get_request_url()`: [https://github.com/gradio-app/gradio/blob/0e6ae52f3d1fcf0716975d7c1bed68e3038b3f66/gradio/route_utils.py#L363-L371](https://github.com/gradio-app/gradio/blob/0e6ae52f3d1fcf0716975d7c1bed68e3038b3f66/gradio/route_utils.py#L363-L371) also fixed to avoid a future bugs?@abidlabs: No `get_request_url` is only used in `get_root_url` where we want the root url of the Gradio app, without any subpaths so I think we should be good!@matrn: I understand that it is used only in `get_root_url` but if I my usage is correct:
```python
from fastapi import Request
from gradio.route_utils import get_request_url

url = get_request_url(
    Request(
        scope={
            "type": "http",
            "headers": [(b"host", b"localhost:7860")],
            "path": "/test/abc",
        }
    )
)
print(url)  # -> http://localhost:7860/test/abc


url = get_request_url(
    Request(
        scope={
            "type": "http",
            "headers": [(b"host", b"localhost:7860"), (b"x-forwarded-host", b"example.com")],
            "path": "/test/abc",
        }
    )
)
print(url)  # -> http://example.com
```
the function `get_request_url` returns only host when `x-forwarded-host` is set which can cause similar bug in the future.@abidlabs: I see your point, yes it would be preferable to refactor this. I'll open a PR@matrn: Great, thank you

Solution patch:
diff --git a/.changeset/some-pants-give.md b/.changeset/some-pants-give.md
new file mode 100644
index 000000000..0c592ba78
--- /dev/null
+++ b/.changeset/some-pants-give.md
@@ -0,0 +1,5 @@
+---
+"gradio": patch
+---
+
+fix:Refactor `get_request_url`
diff --git a/gradio/route_utils.py b/gradio/route_utils.py
index c2d15e32f..f7de88fe7 100644
--- a/gradio/route_utils.py
+++ b/gradio/route_utils.py
@@ -360,7 +360,14 @@ def get_first_header_value(request: fastapi.Request, header_name: str):
     return None
 
 
-def get_request_url(request: fastapi.Request) -> str:
+def get_request_origin(request: fastapi.Request, route_path: str) -> httpx.URL:
+    """
+    Examines the request headers to determine the origin of the request.
+    If the request includes the x-forwarded-host header, it is used directly to determine the origin.
+    Otherwise, the request url is used and the route path is stripped off.
+
+    The returned URL is a httpx.URL object without a trailing slash, e.g. "https://example.com"
+    """
     x_forwarded_host = get_first_header_value(request, "x-forwarded-host")
     root_url = f"http://{x_forwarded_host}" if x_forwarded_host else str(request.url)
     root_url = httpx.URL(root_url)
@@ -368,6 +375,12 @@ def get_request_url(request: fastapi.Request) -> str:
     root_url = str(root_url).rstrip("/")
     if get_first_header_value(request, "x-forwarded-proto") == "https":
         root_url = root_url.replace("http://", "https://")
+
+    route_path = route_path.rstrip("/")
+    if len(route_path) > 0 and not x_forwarded_host:
+        root_url = root_url[: -len(route_path)]
+    root_url = root_url.rstrip("/")
+    root_url = httpx.URL(root_url)
     return root_url
 
 
@@ -418,15 +431,8 @@ def get_root_url(
     if root_path and client_utils.is_http_url_like(root_path):
         return root_path.rstrip("/")
 
-    x_forwarded_host = get_first_header_value(request, "x-forwarded-host")
-    root_url = get_request_url(request)
-
-    route_path = route_path.rstrip("/")
-    if len(route_path) > 0 and not x_forwarded_host:
-        root_url = root_url[: -len(route_path)]
-    root_url = root_url.rstrip("/")
+    root_url = get_request_origin(request, route_path)
 
-    root_url = httpx.URL(root_url)
     if root_path and root_url.path != root_path:
         root_url = root_url.copy_with(path=root_path)
 


Test patch:
diff --git a/test/test_routes.py b/test/test_routes.py
index dca3b01e0..249085dc8 100644
--- a/test/test_routes.py
+++ b/test/test_routes.py
@@ -39,6 +39,7 @@ from gradio.route_utils import (
     FnIndexInferError,
     compare_passwords_securely,
     get_api_call_path,
+    get_request_origin,
     get_root_url,
     starts_with_protocol,
 )
@@ -1786,3 +1787,47 @@ def test_get_api_call_path_generic_call(server, path, expected):
     request = Request(scope)
     path = get_api_call_path(request)
     assert path == expected
+
+
+@pytest.mark.parametrize(
+    "headers, server, route_path, expected_origin",
+    [
+        (
+            {},
+            ("localhost", 7860),
+            "/gradio_api/predict",
+            httpx.URL("http://localhost:7860"),
+        ),
+        (
+            {"x-forwarded-host": "example.com"},
+            ("localhost", 7860),
+            "/gradio_api/predict",
+            httpx.URL("http://example.com"),
+        ),
+        (
+            {"x-forwarded-host": "example.com", "x-forwarded-proto": "https"},
+            ("localhost", 7860),
+            "/gradio_api/predict",
+            httpx.URL("https://example.com"),
+        ),
+        (
+            {
+                "x-forwarded-host": "example.com,internal.example.com",
+                "x-forwarded-proto": "https,http",
+            },
+            ("localhost", 7860),
+            "/gradio_api/predict",
+            httpx.URL("https://example.com"),
+        ),
+    ],
+)
+def test_get_request_origin_with_headers(headers, server, route_path, expected_origin):
+    scope = {
+        "type": "http",
+        "headers": [(k.encode(), v.encode()) for k, v in headers.items()],
+        "server": server,
+        "path": route_path,
+    }
+    request = Request(scope)
+    origin = get_request_origin(request, route_path)
+    assert origin == expected_origin