[BUG] Embedded download button does not work if have PublicSpace -> Private Space

When click (embedded) button[aria-label=“Download”] of generated image it will not download,but show the generated picture!!!

Must remove in URL everything untill first “file=”

https://publicspace.hf.space/gradio_api/proxy=https://privatespace.hf.space/gradio_api/file=https://privatespace.hf.space/gradio_api/file=/tmp/gradio/16343a2509eafc78454c033343f660b008a72490d87b175fbd550f07a88b7b89/somefiilenameofjustgeneratedpic.webp

1 Like

iframe one?


This behavior is expected once you combine three things: (1) Hugging Face Spaces are iframe-embedded, (2) your “Download” UI is ultimately an HTML link, and (3) the image URL becomes effectively cross-origin because it is served through a proxy chain.

Your URL shows the whole chain:

publicspace.hf.space
  /gradio_api/proxy= https://privatespace.hf.space/gradio_api/file= https://privatespace.hf.space/gradio_api/file=/tmp/gradio/.../image.webp

That “proxy= … file= … file=” nesting is the key smell: the download link is not a clean “same-origin downloadable file”. It is a proxied, rewritten asset URL.

Below are the main causes, how to verify each one quickly, and the fixes that actually stick.


Background: why this happens on Hugging Face in particular

Spaces are embedded in an iframe on the Hugging Face page

When you open a Space via https://huggingface.co/spaces/<user>/<space>, Hugging Face embeds the real app (the https://*.hf.space/ origin) inside an iframe. Hugging Face documents this explicitly. (Hugging Face)

So even before you add a private Space, you are already in an iframe context.

Embedding requires a public Space

Hugging Face’s embed doc states that to embed a Space, it must be public. That is why many people do the “public wrapper Space → private Space” pattern. (Hugging Face)


Cause 1: the browser ignores <a download> for cross-origin URLs

Most “download icons” in web UIs are implemented as an <a href="..."> link with a download attribute, or equivalent logic.

Modern browsers intentionally ignore the download attribute in cross-origin situations to prevent data leakage and abuse. Chrome formalized this in Chrome 65. (Chrome for Developers)

So if the final resource is treated as cross-origin (and in your case it is, because it ultimately points to the private Space origin or a proxy of it), the browser is allowed to do the “safe” thing: navigate to the image (preview it) instead of downloading.

This is also exactly what the Gradio team calls out in a related bug: download buttons fail in iframes because the embedded app and the parent have different origins and <a download> does not work across them. (GitHub)

Why your setup triggers cross-origin

  • Public Space origin: publicspace.hf.space
  • Private Space origin: privatespace.hf.space
  • Your download URL contains a proxy-to-private chain.

Different hostnames means different origins. Same-origin policy is strict about this. (MDN Web Docs)


Cause 2: proxy/file routing returns “inline image” behavior

Even if the UI tries to force download, the response often comes back as:

  • Content-Type: image/webp
  • no Content-Disposition: attachment

Browsers will typically render images inline in a new tab when you navigate to such a URL.

In a public→private setup, it is common that the outer proxy route behaves like a normal image fetch, not like a “download endpoint”.

This category of “proxy breaks file access / download” issues exists in Gradio generally (proxy environments change URL handling and file access rules). (GitHub)


Cause 3: Gradio gr.load() + private Space outputs are a known rough edge

Multiple reports match the broader pattern: the UI loads, inference runs, but outputs that are files/images fail or behave oddly when the loaded Space is private.

Examples:

  • “loading a private space from a public one… interface loads, output retrieval errors” (GitHub)
  • “outputs from private space result in a 404” (Hugging Face Forums)
  • “gr.load private space… images don’t work” (Hugging Face Forums)
  • “public space loads private space… fails to load images” (Hugging Face Forums)

Your symptom is slightly different (preview instead of download), but it is the same underlying theme: file outputs are the fragile part when you cross Space visibility boundaries.


Cause 4: the “double file=” URL is probably a rewrite bug (or double-wrapping)

Your URL includes:

  • .../file=https://privatespace.../file=/tmp/gradio/...

That means something treated an already-constructed file URL as a string and then wrapped it again.

This can happen when:

  • the loaded app returns a URL string instead of a file object/path
  • the wrapper app (or Gradio front-end) tries to “make it accessible” and prepends another file/proxy handler

You can usually confirm this by inspecting the JSON returned by the predict call and seeing whether the “image” is a structured file object vs a raw URL string.


How to confirm the exact cause in DevTools

Do these three checks once. They remove guesswork.

1) Inspect the element behind the download icon

  • Right click the download icon → Inspect.
  • Look for an <a> tag and see its href.
  • If it’s your long proxy URL, you already know the browser is likely to ignore “download” for cross-origin. (Chrome for Developers)

2) Network tab: compare headers

Click download, then inspect the response headers for that request.

If you do not see something like Content-Disposition: attachment, the browser will happily preview it.

(And in cross-origin situations, even having download on the link often won’t help.)

3) Try the “stripped to first file=” URL and compare

When you manually strip to the first file=, you bypass one wrapper. If the behavior changes, that is consistent with “outer proxy URL is the problem”.


Solutions that work reliably

I’m listing these in the order that tends to be most robust on Spaces.

Solution 1 (recommended): re-host the output file on the public Space, then download locally

Instead of trying to download the private Space’s /tmp/gradio/... file through a proxy URL, do this:

  1. Public Space calls private Space (server-to-server) to generate.
  2. Public Space downloads the bytes (using its token).
  3. Public Space exposes the result as a local file and uses gr.DownloadButton.

Why it works:

  • The downloadable URL becomes same-origin (publicspace.hf.space).
  • You avoid cross-origin <a download> restrictions. (Chrome for Developers)
  • You avoid nested proxy=...file=...file=... URLs.

Gradio explicitly provides DownloadButton for this use case. (Gradio)

Also, use Gradio’s Python client to call remote apps cleanly. (Gradio)

Solution 2: add a custom “download endpoint” that sets attachment headers

If you can add a small FastAPI route (common in Docker Spaces), you can make:

/download?path=<id>

and respond with Content-Disposition: attachment.

This approach avoids relying on the browser’s cross-origin download attribute behavior. Chrome’s change is specifically about ignoring <a download> cross-origin, not about forbidding downloads when servers mark attachments. (Chrome for Developers)

Solution 3: return bytes (or base64) from the private Space instead of a temp file URL

If you control the private Space app:

  • return the image as bytes (or base64)
  • have the public Space write it to disk
  • then offer it via DownloadButton

This avoids /gradio_api/file=/tmp/gradio/... entirely.

Solution 4: if you embed the Space on your own site, ensure iframe sandbox allows downloads

If you embed *.hf.space in an iframe on your own page and you set a restrictive sandbox, downloads can be blocked unless you allow them. This is a separate but common pitfall.

(If you are only using the Hugging Face Space page, you typically do not control this, so Solutions 1–3 are the practical ones.)


Pitfalls to avoid

Pitfall A: depending on /tmp/gradio/... URLs across machines or replicas

Even when not private, /tmp is ephemeral and can break across restarts. Keep downloads stable by re-hosting or regenerating.

Pitfall B: assuming gr.File or “raw URL string” outputs behave the same across versions

Gradio’s file access and serving rules tightened over time, and there are reports of file serving changes and breakages behind proxies and in newer versions. (Gradio)

Pitfall C: “public wrapper loads private UI” is convenient but fragile for outputs

The volume of threads on this pattern should be treated as a warning sign. (Hugging Face Forums)


If you want to file a good upstream bug report

Include:

  • the exact URL shape showing proxy=...file=...file=...

  • Gradio version on both Spaces

  • DevTools screenshots of:

    • the download link element
    • response headers (especially Content-Disposition)
  • a minimal pair of Spaces: one public wrapper, one private generator

Reference the existing Gradio iframe download issue as related context. (GitHub)


Summary bullets

  • Root cause is usually cross-origin download restrictions plus proxy-wrapped file URLs. (Chrome for Developers)
  • Hugging Face adds an iframe layer and public→private introduces multiple origins. (Hugging Face)
  • Best fix is to fetch the file server-side in the public Space and serve it locally via DownloadButton. (Gradio)
  • The “double file=” indicates double URL rewriting, often from returning URLs instead of file objects.

Uau, so many text and words without simple and fast solution (exactly AI way)

Solution (human=quick and fast and clean): Just replace gr.Image(…) with gr.Gallery(…)!

Anyway, thank you for your effort

1 Like

Sorry for AI gen answer. Oh my. I had no idea that solution existed…:innocent:
Since I didn’t understand why it worked, I’ll add a note for future reference…
I see…


Yes. Swapping the output from gr.Image(...) to gr.Gallery(...) is a real, quick workaround for this specific “embedded public Space → private Space” download behavior, because Gallery’s download is implemented as a dedicated “download selected image” toolbar action. The Gallery docs explicitly describe that built-in download button. (Gradio)

The fast fix

Minimal diff

  • Replace the output component.
  • Return a list of images (even if it is just one).
import gradio as gr

def run(prompt):
    img_path = generate_somehow(prompt)  # filepath, PIL image, or numpy
    return [img_path]  # Gallery expects a list

with gr.Blocks() as demo:
    prompt = gr.Textbox()
    out = gr.Gallery(columns=1, buttons=["download", "fullscreen"])  # download on toolbar
    gr.Button("Go").click(run, prompt, out)

demo.launch()

Notes:

  • For one image, use columns=1.
  • If you want captions, return [(img, "caption")] (still a list).

Why this “Image fails but Gallery works” can happen

1) Downloads in iframes are a known weak spot in Gradio

Gradio has an explicit bug report that download buttons don’t work in iframes, tied to how browsers handle downloads and origins. (GitHub)
Hugging Face Spaces are commonly consumed via embeds, and “public wrapper → private Space” introduces extra proxying and URL rewriting, which makes the download path more fragile.

2) Gallery’s download path has had targeted work and fixes

Historically, Gallery download behavior was added and then fixed multiple times:

  • A feature request existed because Gallery did not have download initially. (GitHub)
  • Gradio’s changelog notes adding a download button for selected images in Gallery. (Gradio)
  • Specific Gallery download bugs were fixed (caption shape bug, and “download broken” bug). (GitHub)

So depending on your Gradio version, Gallery may be “on the newer, patched code path” while Image still hits the behavior you are seeing.

Caveat: Gallery download was buggy in some versions

If you are on older Gradio builds, Gallery download itself could be broken (for example downloading the parent HTML, or behaving differently with captions). (GitHub)
If your “Gallery fix” ever stops working, this is usually why.

Most robust solution if you want “always works”

Use a dedicated DownloadButton and hand it a local filepath you control in the public Space. That avoids relying on embedded toolbar download behavior. (Gradio)
This is the “boring but reliable” route when proxies and private Spaces are involved.


Summary

  • Quick workaround: use gr.Gallery as output and return [image]. (Gradio)
  • Why it works: Gallery’s download button is a separate implementation and has had multiple targeted fixes. (Gradio)
  • If you need maximum reliability: use gr.DownloadButton with a local file path. (Gradio)

I was wrong - the major problem will not resolve with replacing of gr.Image() with gr.Gallery and when press embedded download button (button[aria-label=“Download”]) of just generated picture (in Image or Gallery) it will not download actual file, but will open the image in the current window and you will lose all generated images (will reset UI)

The only working solution is to use window.open(img.src, “_blank”);

I have made many tries like hidden form submit; fetch(img.src)..then(blob =>; URL.createObjectURL(blob); canvas.toBlob(blob => …

without any success = can’t bypass browser security …

Again, thank you John6666 for your detailed info

1 Like