Skip to content

Fix private Space file URLs in gr.load() and the JS client - #13816

Draft
abidlabs wants to merge 8 commits into
mainfrom
fix/issue-11605-cannot-load-images-from-private-space-with-gr-lo
Draft

Fix private Space file URLs in gr.load() and the JS client#13816
abidlabs wants to merge 8 commits into
mainfrom
fix/issue-11605-cannot-load-images-from-private-space-with-gr-lo

Conversation

@abidlabs

@abidlabs abidlabs commented Sep 2, 2026

Copy link
Copy Markdown
Member

Description

Private-Space file values now work consistently through both gr.load() and @gradio/client, including initial component values, event outputs, values reused by later events, Gallery data, and streaming media.

For gr.load(), upstream files keep their remote filesystem path while exposing one authenticated loader-proxy URL to the browser. Initial component values are normalized after proxy_url is installed, and browser-facing proxy URLs are converted back to upstream inputs for later events. Explicit external HTTP URLs remain direct.

For @gradio/client, returned gradio.FileData URLs on the connected private Space's own origin receive the scoped Space JWT already obtained during connection. This is applied to initial/refreshed config, prediction and streaming data, render events, and component-server outputs. External URLs are intentionally unchanged, and the upstream path is retained so outputs can be reused as inputs. Signed HLS playlist requests also propagate the signature to their segment URLs, since media players do not inherit the playlist query string automatically.

Closes: #11605

AI Disclosure

  • I used AI to reproduce the issue, draft the implementation and tests, and prepare this PR description. I reviewed every changed line and verified the behavior locally and against deployed private/public Spaces.
  • I did not use AI

🎯 PRs Should Target Issues

This PR targets #11605. I checked for overlapping open PRs by issue number and key terms and found none.

Testing and Formatting Your Code

  • Full client/js browser suite — 189 passed, 10 skipped
  • Full client/js Node suite — 174 passed, 25 skipped
  • test_processing_utils.py, test_blocks.py, test_helpers.py, and test_routes.py — 396 passed
  • Focused post-merge JS browser suite — 47 passed, 8 skipped
  • Focused post-merge JS Node suite — 51 passed, 4 skipped
  • Focused post-merge route tests — 10 passed, 170 deselected
  • pnpm build, Prettier, ESLint, TypeScript, and focused Ruff checks — passed
  • GitHub Actions — all checks passed
  • Live JS-client verification against the private source Space:
    • image URL carries __sign and fetches without a bearer header (200 image/png)
    • generated image reused as a later input returns Received the image back as an input: 480×320
    • HLS playlist and both generated AAC segment URLs carry __sign and fetch without bearer headers (200)
  • Logged-out browser verification of the after Space:
    • initial Image, initial Gallery, and event Image render at 480×320
    • image round-trip returns Received the image back as an input: 480×320
    • streaming audio reaches ready state 4

The demo below is kept untracked and is also deployed as the linked before/after Spaces.

import os

import gradio as gr


demo = gr.load(
    "abidlabs/gradio-11605-private-source",
    src="spaces",
    token=os.getenv("HF_TOKEN"),
)

demo.launch()

@gradio-pr-bot

gradio-pr-bot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

🪼 branch checks and previews

Name Status URL
Spaces ready! Spaces preview
Website ready! Website preview
🦄 Changes failed! Workflow log

Install Gradio from this PR

pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/490ab3ecfc2d2fb13cfbec5bd62a01527f0f95be/gradio-6.26.0-py3-none-any.whl

Install Gradio Python Client from this PR

pip install "gradio-client @ git+https://github.com/gradio-app/gradio@490ab3ecfc2d2fb13cfbec5bd62a01527f0f95be#subdirectory=client/python"

Import Gradio JS Client from this PR via CDN

import { Client } from "https://huggingface.co/buckets/gradio/npm-previews/resolve/490ab3ecfc2d2fb13cfbec5bd62a01527f0f95be/browser.js";

@gradio-pr-bot

gradio-pr-bot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

🦄 change detected

This Pull Request includes changes to the following packages.

Package Version
@gradio/client patch
gradio patch

  • Fix private Space file URLs in gr.load() and the JS client

Something isn't right?

  • Maintainers can change the version label to modify the version bump.
  • If the bot has failed to detect any changes, or if this pull request needs to update multiple packages to different versions or requires a more comprehensive changelog entry, maintainers can update the changelog file directly.

@abidlabs

abidlabs commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Before / after Spaces

  • Before fix — latest released Gradio and @gradio/client
  • After fix — this PR's Gradio wheel, Python client, and JS client preview

Both Spaces load the same private source and use byte-identical loader app.py files.

The first section exercises gr.load() automatically. In the JavaScript client section, paste an HF read token with access to the private source, then click Load private files with the JavaScript client. That function runs entirely in the browser; the token is not sent to either public Space's backend. After the outputs appear, click Send the JS-client image back to the private Space to verify the second-event input path.

Use case Before After
gr.load() initial Image and pre-filled Gallery Raw private URLs; images do not render logged out Single loader-proxy URLs; both render at 480×320
gr.load() Gallery .select() data Selection succeeds Selection succeeds through the normalized value path
gr.load() event Image Double-proxied URL; image does not render Single proxy URL; image renders at 480×320
gr.load() output reused by a second event Upstream error Returns Received the image back as an input: 480×320
gr.load() streaming audio Double /stream/ URL; media stays at ready state 0 Single proxy URL; media reaches ready state 4
JS client initial Image and Gallery config values URLs are unsigned; browser media requests fail Same-origin FileData URLs carry a scoped Space signature and render
JS client event Image URL is unsigned; browser media request fails Signed URL fetches logged out as 200 image/png
JS client output reused by a second event No first-party browser-loadable URL Returns Received the image back as an input: 480×320
JS client streaming audio Playlist/segments cannot be loaded by the media element Playlist and every AAC segment are signed and return 200

Live verification used the exact PR JS preview and confirmed that external URLs remain unchanged while all private same-origin file cases above are signed.

@abidlabs
abidlabs requested review from dawoodkhan82 and hysts and a balanced review from Copilot September 2, 2026 22:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation and tests are sound; the remaining changeset comment is a non-blocking nit.

Pull request overview

Fixes authenticated file proxying for private Spaces loaded via gr.load().

Changes:

  • Preserves upstream paths for loaded Space files.
  • Keeps external HTTP URLs direct.
  • Adds synchronous and asynchronous regression tests.
File summaries
File Description
test/test_processing_utils.py Adds regression coverage for private files and external URLs.
gradio/processing_utils.py Corrects loaded-Space proxy URL construction.
.changeset/silly-hairs-tie.md Adds a hand-authored changeset that should be removed.
Review details

Suppressed comments (1)

.changeset/silly-hairs-tie.md:5

  • Remove this hand-authored changeset. AGENTS.md:39 states that the GitHub Action generates changesets from the PR title; keeping this file causes it to override the generated changelog entry.
---
"gradio": patch
---

fix:Fix private Space file proxying in gr.load()
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@abidlabs
abidlabs marked this pull request as ready for review September 4, 2026 04:18

@hysts hysts left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The double /file= is the bug and the fix is in the right place. Repro pair on this wheel, if it is useful: gradio-13816-loader is a bare gr.load() of the private gradio-13816-private-source. Viewed logged out, the bottom row fills correctly when you press the button, so the click path works.

Two things I think should be fixed before this lands. The first is inline: once payload.path keeps the upstream filesystem path, any value travelling back as an input reaches gradio_client._upload_file as if it were a local file, so any second event fails. Not private-specific, and there is a traceback from that same Space. The second is below, because gradio/blocks.py is not in the diff and there is no line to anchor to. Two smaller points are inline as well.

Initial values still carry the raw private URL. Blocks.from_config pops value before construction and assigns it straight back afterwards, so it never reaches move_files_to_cache:

gradio/gradio/blocks.py

Lines 1276 to 1284 in a0e1111

postprocessed_value = block_config["props"].pop("value", None)
constructor_args = cls.recover_kwargs(block_config["props"])
block = cls(**constructor_args)
if postprocessed_value is not None:
block.value = postprocessed_value # type: ignore
block_proxy_url = block_config["props"]["proxy_url"]
block.proxy_url = block_proxy_url

A private Space with gr.Image(value=...) or a pre-filled gr.Gallery still shows a logged-out viewer a broken image after this PR. That is the top row in the loader above, which stays broken next to the bottom row that this PR fixed. Pre-filled galleries as inputs are common enough that I would not call it an edge case.

Your change is what makes that fixable. Running the value through move_files_to_cache once proxy_url is set, at line 1284 above, gives the correct single-proxy URL on top of this PR, and the old double-proxy without it:

block_proxy_url = block_config["props"]["proxy_url"]
block.proxy_url = block_proxy_url
if postprocessed_value is not None:
    block.value = processing_utils.move_files_to_cache(
        block.value, block, postprocess=True
    )

The JS client case is untouched, and not something for this PR. Poli reported the same symptom through @gradio/client on the issue. Nothing in processing_utils reaches it: the client has no server in the loop to re-sign the request, which is exactly what makes the Python fix work. Client.fetch() already attaches the token, so a consumer can fetch and build a blob URL today, but there is no first-party answer. Flagging it so it does not get lost, whether that means a separate issue or something in this one.

Comment thread gradio/processing_utils.py Outdated
payload.url
and postprocess
and client_utils.is_http_url_like(payload.url)
and not block.proxy_url

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right for display, but it also changes what the next event receives.

from_config sets dependency["preprocess"] = False, so preprocess_data passes inputs_serialized to the client end-to-end fn verbatim, reaching Endpoint.process_input_files and then _upload_file. That skips everything when the path is an http URL and otherwise treats it as a local file. Before this PR path was the absolute upstream URL, so the local branch never ran and the upstream re-fetched the file. Now path belongs to another container.

Live, on the loader Space linked above. /fill returns the corrected URL:

path = /tmp/gradio/916cf043.../cheetah1.jpg
url  = .../gradio_api/proxy=https://<source>.hf.space/gradio_api/file=/tmp/gradio/916cf043.../cheetah1.jpg

Handing that same payload to /describe, which takes that component as an input and is what the browser does on any second event:

File ".../gradio_client/client.py", line 1382, in _upload_file
    if os.path.getsize(file_path) > max_file_size:
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/gradio/916cf043.../cheetah1.jpg'

Public loaded Spaces hit this too: the condition is that the two apps do not share a filesystem. It passes locally, where both sides are one machine and the path exists, which is what makes it easy to miss.

Same root cause, narrower consequence: .select() handlers go through make_event_data_fn, and special_args runs check_all_files_in_cache, which passes http-like paths and otherwise requires the path to sit under get_upload_folder(). Measured with both payload shapes:

loader GRADIO_TEMP_DIR=/tmp/gradio            before=ok  after=ok
loader GRADIO_TEMP_DIR=/data/gradio           before=ok  after=raises
loader GRADIO_TEMP_DIR=/var/folders/../T/…    before=ok  after=raises

Space-to-Space is fine; gr.load() from macOS, or anything setting GRADIO_TEMP_DIR, raises "File ... is not in the cache folder" on every selection.

One shape that keeps both directions: restore payload.path as you do here, keep payload.url too, and let _upload_file prefer the URL when there is one. Here or in the client, your call.

Minor, while you are in this hunk: the comment above still explains only the original three conjuncts. A line saying a proxy_url means the URL came from the upstream app rather than the developer, and is not browser-reachable for a private Space, would save the next reader a trip through from_config.

f"{API_PREFIX}/stream/" if payload.is_stream else f"{API_PREFIX}/file="
)
if block.proxy_url:
if block.proxy_url and not client_utils.is_http_url_like(payload.path):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes who fetches external URLs. Before, an external URL on a proxied component went out as /gradio_api/proxy=<space>/gradio_api/file=<url>, so the upstream fetched it through secure_url_stream_response and the bytes reached the viewer through two servers. Now the URL is emitted verbatim and the viewer's browser contacts that host directly, with the usual IP and referrer exposure.

I do not think that is wrong. It is what a plain app does for gr.Image("https://..."), and the old behaviour only existed as a side effect of the double-proxy bug. But test_move_files_to_cache_does_not_proxy_external_urls locks it in as intended, so it is worth a sentence in the description and a comment here rather than arriving silently.

)

for result in (sync_result, async_result):
assert result["path"] == remote_path

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both new tests are postprocess=True only, which is the half that works. Inline 1 lives in the direction they do not cover, and neither reaches a gr.load() round trip, since proxy_url is set on the component by hand rather than through Blocks.from_config.

Two additions would have caught it: feed result back through async_move_files_to_cache(..., postprocess=False) and assert check_all_files_in_cache(result) does not raise with GRADIO_TEMP_DIR pointed away from /tmp/gradio; and assert what Endpoint.process_input_files does with result.

Also worth three lines: is_stream=True. The /gradio_api/stream/ prefix goes through the same condition and was double-proxied the same way, so this fixes streaming outputs from a loaded Space too, currently unguarded.

@abidlabs

abidlabs commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Hmm I see thanks @hysts let me convert this back to draft while I investigate if there's a better way

@abidlabs
abidlabs marked this pull request as draft September 4, 2026 18:13
@abidlabs abidlabs changed the title Fix private Space file proxying in gr.load() Fix private Space file URLs in gr.load() and the JS client Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot load images from private space with gr.load() if not logged in

4 participants