From 31c415bab5cd14627209ab57437f44a6c6ba1978 Mon Sep 17 00:00:00 2001 From: hannahblair Date: Wed, 12 Aug 2026 15:53:24 +0100 Subject: [PATCH 1/4] add LocalContext.request in /component_server --- gradio/routes.py | 14 ++++++++++---- test/test_routes.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/gradio/routes.py b/gradio/routes.py index 87b8665c22..d35fd81383 100644 --- a/gradio/routes.py +++ b/gradio/routes.py @@ -71,7 +71,7 @@ utils, ) from gradio.brotli_middleware import BrotliMiddleware -from gradio.context import Context +from gradio.context import Context, LocalContext from gradio.data_classes import ( CancelBody, ComponentServerBlobBody, @@ -1718,12 +1718,18 @@ async def component_server( request, # type: ignore None, ) - if inspect.iscoroutinefunction(fn): - return await fn(*processed_input) - else: + # So `gradio_client.Client` in a server fn can forward `x-ip-token` + # (ZeroGPU quota) — regular event handlers get this via + # `get_function_with_locals`, this path bypasses that wrapping. + LocalContext.request.set(request) # type: ignore + try: + if inspect.iscoroutinefunction(fn): + return await fn(*processed_input) return await anyio.to_thread.run_sync( fn, *processed_input, limiter=app.get_blocks().limiter ) + finally: + LocalContext.request.set(None) @router.get( "/queue/status", diff --git a/test/test_routes.py b/test/test_routes.py index 252e314178..db46b40725 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -2553,6 +2553,39 @@ def get_url(self, request: gr.Request): assert response.json()["_url"].endswith("/gradio_api/component_server") +def test_server_fn_forwards_x_ip_token_via_local_context(): + """/component_server must set LocalContext.request so gradio_client.Client + can forward the caller's x-ip-token to downstream ZeroGPU Spaces.""" + import requests + + from gradio.components.base import server + from gradio.context import LocalContext + + def get_ip_token(self, _data): + req = LocalContext.request.get(None) + return req.headers.get("x-ip-token") if req else None + + tb = gr.Textbox() + tb.get_ip_token = server(get_ip_token) # type: ignore + iface = gr.Interface(lambda x: x, inputs=tb, outputs="text") + component_id = next( + c["id"] for c in iface.config["components"] if c["type"] == "textbox" # type: ignore + ) + _, local_url, _ = iface.launch(prevent_thread_lock=True) + response = requests.post( + f"{local_url}/gradio_api/component_server", + json={ + "session_hash": "foo", + "component_id": component_id, + "fn_name": "get_ip_token", + "data": json.dumps({}), + }, + headers={"x-ip-token": "test-token"}, + ) + assert response.status_code == 200 + assert response.json() == "test-token" + + def test_slugify(): items = ( ("Hello, World!", "hello-world"), From ebecc73e8c5c291ecb8876740d5f7760a0f7a321 Mon Sep 17 00:00:00 2001 From: gradio-pr-bot Date: Wed, 12 Aug 2026 14:59:05 +0000 Subject: [PATCH 2/4] add changeset --- .changeset/fresh-lizards-knock.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fresh-lizards-knock.md diff --git a/.changeset/fresh-lizards-knock.md b/.changeset/fresh-lizards-knock.md new file mode 100644 index 0000000000..89efcb198c --- /dev/null +++ b/.changeset/fresh-lizards-knock.md @@ -0,0 +1,5 @@ +--- +"gradio": minor +--- + +feat:workflow: forward x-ip-token so zerogpu spaces bill the caller From b874861986816e3f656d46909aab551a2088e670 Mon Sep 17 00:00:00 2001 From: hannahblair Date: Wed, 12 Aug 2026 16:15:12 +0100 Subject: [PATCH 3/4] format --- test/test_routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_routes.py b/test/test_routes.py index db46b40725..131c09bbab 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -2569,7 +2569,9 @@ def get_ip_token(self, _data): tb.get_ip_token = server(get_ip_token) # type: ignore iface = gr.Interface(lambda x: x, inputs=tb, outputs="text") component_id = next( - c["id"] for c in iface.config["components"] if c["type"] == "textbox" # type: ignore + c["id"] + for c in iface.config["components"] + if c["type"] == "textbox" # type: ignore ) _, local_url, _ = iface.launch(prevent_thread_lock=True) response = requests.post( From da76169521194fc0722bdd05d0382964f3e0b92a Mon Sep 17 00:00:00 2001 From: hannahblair Date: Thu, 13 Aug 2026 14:28:30 +0100 Subject: [PATCH 4/4] refactor --- gradio/routes.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/gradio/routes.py b/gradio/routes.py index d35fd81383..5ec81c353d 100644 --- a/gradio/routes.py +++ b/gradio/routes.py @@ -71,7 +71,7 @@ utils, ) from gradio.brotli_middleware import BrotliMiddleware -from gradio.context import Context, LocalContext +from gradio.context import Context from gradio.data_classes import ( CancelBody, ComponentServerBlobBody, @@ -1718,18 +1718,19 @@ async def component_server( request, # type: ignore None, ) - # So `gradio_client.Client` in a server fn can forward `x-ip-token` - # (ZeroGPU quota) — regular event handlers get this via - # `get_function_with_locals`, this path bypasses that wrapping. - LocalContext.request.set(request) # type: ignore - try: - if inspect.iscoroutinefunction(fn): - return await fn(*processed_input) - return await anyio.to_thread.run_sync( - fn, *processed_input, limiter=app.get_blocks().limiter - ) - finally: - LocalContext.request.set(None) + fn = utils.get_function_with_locals( + fn, + app.get_blocks(), + event_id=None, + in_event_listener=False, + request=request, # type: ignore + state=state, + ) + if inspect.iscoroutinefunction(fn): + return await fn(*processed_input) + return await anyio.to_thread.run_sync( + fn, *processed_input, limiter=app.get_blocks().limiter + ) @router.get( "/queue/status",