Skip to content

ui.download.from_url() opens url for download and ignores filename #4723

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
3 tasks done
morrowrasmus opened this issue May 9, 2025 · 11 comments · May be fixed by #4779
Open
3 tasks done

ui.download.from_url() opens url for download and ignores filename #4723

morrowrasmus opened this issue May 9, 2025 · 11 comments · May be fixed by #4779
Labels
analysis Status: Requires team/community input bug Type/scope: A problem that needs fixing
Milestone

Comments

@morrowrasmus
Copy link

morrowrasmus commented May 9, 2025

First Check

  • I added a very descriptive title here.
  • This is not a Q&A. I am sure something is wrong with NiceGUI or its documentation.
  • I used the GitHub search to find a similar issue and came up empty.

Example Code

from nicegui import ui

ui.button(icon='download', on_click=lambda: ui.download.from_url('https://nicegui.io/logo.png', filename='my_custom_name.png'))

ui.run()

Description

Using ui.download.from_url() with the filename parameter specified, I expect that the downloaded is triggered directly (and not opened in a new tab first) and that the file will be served with whatever filename was passed. However, the file is opened in a new tab instead, and if this triggers a download, it retains the original name of the server.

In the example found in the documentation, the same file is directly downloaded and not opened in a new tab. The example does not show the usage of the filename-parameter.

NiceGUI Version

2.16.1.dev0

Python Version

Python 3.13.0

Browser

Chrome

Operating System

Windows

Additional Context

Tested on Chrome and Edge

@evnchn
Copy link
Collaborator

evnchn commented May 9, 2025

Hi @morrowrasmus. Thank you for the report. Sorry but it is not NiceGUI issue.

download only works for same-origin URLs, or the blob: and data: schemes.

https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#download

Internally, NiceGUI spawns an <a> tag to do the download, so shortcomings shared by ths <a download="..."> attribute, NiceGUI also suffers from it.

function download(src, filename, mediaType, prefix) {
const anchor = document.createElement("a");
if (typeof src === "string") {
anchor.href = src.startsWith("/") ? prefix + src : src;
} else {
anchor.href = URL.createObjectURL(new Blob([src], { type: mediaType }));
}
anchor.target = "_blank";
anchor.download = filename || "";
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
if (typeof src !== "string") {
URL.revokeObjectURL(anchor.href);
}
}


Workarounds could be (speculation but reasonable):

  • To use JavaScript to fetch the data and then do a file download, but then we would face CORS issue.
  • To have the Python backend to fetch the data then download, but then everything goes through your IP and you may become rate limited.

@evnchn evnchn added the bug Type/scope: A problem that needs fixing label May 9, 2025
@falkoschindler
Copy link
Contributor

@evnchn Great analysis!

Sorry but it is not NiceGUI issue.

One could argue that it is an issue if we promise to trigger a download and provide a parameter called "filename":

filename: name of the file to download (default: name of the file on the server)

If "download only works for same-origin URLs", maybe we need to warn or raise if the origin doesn't match?

@falkoschindler falkoschindler added the analysis Status: Requires team/community input label May 9, 2025
@evnchn
Copy link
Collaborator

evnchn commented May 9, 2025

Fair point.

I think, maybe it can be a combination of the two approaches:

  • Documentation update
  • Warn when the URL is not a same-origin one (but it would be difficult to detect https://nicegui.io/icon.png is same-origin on https://nicegui.io, so perhaps we should enforce the use of /icon.png)

@falkoschindler
Copy link
Contributor

Ok, what about the following proposal:

  • Extend the documentation of ui.download to inform about these limitations.
  • Ensure same origin in JavaScript before creating the <a> tag.
  • If it isn't the same origin, emit a log message to the server using a new emitLog JavaScript function. It emits a new global "log" event including an optional log level. The server listens for such log messages and prints them to the terminal.

@falkoschindler falkoschindler added this to the 2.x milestone May 13, 2025
@evnchn
Copy link
Collaborator

evnchn commented May 13, 2025

First point I agree, but second and third point I have hesitations about.

I'm guessing your reasoning for a same-origin test on the client-side (creating the new emitLog function just around it), is for NiceGUI to check if ui.download.from_url('https://nicegui.io/icon.png') is same-origin, when compared to the origin which it is accessed from, which could be https://nicegui.io, http://localhost:8080, or http://192.168.1.39:8080

But, I think passing ui.download.from_url('https://nicegui.io/icon.png') is generally malpractice in the beginning, as it makes the code vulnerable to breaking if and only if accessed from different origins. As such our newly-added warning would also not show up in the developer's console, if the developer primarily tests on one origin http://localhost:8080, but deploys on another origin https://mysite.com.

Since, by all means the best practice is ui.download.from_url('/icon.png'), therefore we should put the same-origin test to the server-side, warning if the path is not a relative or absolute URL path in the same origin.


Meanwhile, I think it should be a warn, not an error, since:

  • In some enterprise or embedded environments, I have heard of stories where they'll ship modified copies of Chromium and/or apply special flags to allow it to download even when cross-origin.
    • As such, NiceGUI should not be more restrictive than the browser.
  • Plus, it can still enable best-effort processing by opening a new tab and having the user download from there.
  • Also avoids raising error in a function which doesn't raise any in the past, which otherwise constitutes a breaking change.

@falkoschindler
Copy link
Contributor

I don't think we should enforce relative URLs. Take this ZIP for example, which works just fine:

zip = 'https://github.com/zauberzeug/nicegui/archive/refs/heads/main.zip'
ui.button('Download').on_click(lambda: ui.download.from_url(zip))

By the way: The proposed warning should only be shown if the user defined a custom filename which will be ignored.

@morrowrasmus
Copy link
Author

Thanks for the explanation - we've ended up implementing a workaround to serve the filename we want that works well, so I hope you don't land on enforcing same-origin downloads. We are dependent on being able to serve external files in the application we are building as we are hosting the user-facing GUI one place and the file server another.

Updated documentation and a warning if trying to set a custom filename when attempting to serve a cross-origin file would have been sufficient for me to understand the limitations here.

@evnchn
Copy link
Collaborator

evnchn commented May 13, 2025

Take this ZIP for example, which works just fine

If I am not mistaken, what's happening is, the browser is not abiding by your download attribute, but it can't open a ZIP in new tab and view it either since browsers don't (yet) have the ability to open ZIP files, so it has no choice but to download the file. It still doesn't abide by your custom filename.

If you replace the ui.download.from_url with ui.navigate.to, it should also download the ZIP.


But I don't think your argument and the warning quite connent, since if you do this code:

zip = 'https://mysite.com/files/main.zip'
ui.button('Download').on_click(lambda: ui.download.from_url(zip, filename='mysite_main.zip'))

Then this would work from mysite.com origin but not any-other-origin.com

So, if the goal is as simple as catching whether filename parameter can break under any circumstance, then screening for absolute ('https://mysite.com/main.zip') URLs in favor of relative URLs ('main.zip' / '/files/main.zip') should be sufficient.

@evnchn
Copy link
Collaborator

evnchn commented May 13, 2025

And, we should also have the relative URL check, if we pass in an URL, for which it is very obvious from the URL that it is downloading something which the browser can display and thus will not download but open in new tab.

So, relative URL check is ran on:

  • ui.download.from_url('https://mysite.com/obviously_a_png.png') since no file is downloaded, rather browser opens in new tab instead
  • ui.download.from_url('https://mysite.com/idk_if_this_is_png_or_zip', filename='hello.bin') since filename (thus the underying download attribute) is disregarded if origin != mysite.com

@evnchn
Copy link
Collaborator

evnchn commented May 13, 2025

So a bit like this?

Image

@evnchn
Copy link
Collaborator

evnchn commented May 13, 2025

In retrospect, I'd classify ui.download.from_url to be "happens to work", if server returns content type which the browser can't view and thus must download, and no filename is passed.

If that's the case, seeing the other 3 warning cases, I'm more inclined to always warning if the URL is not relative.

Those who really want no warning can simply do ui.navigate.to(..., new_tab=True) if they want to attempt cross-origin download by betting on the server serving non-viewable content.

They must use relative URLs for same-origin download though. That's non-negotiable because that's web development best practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
analysis Status: Requires team/community input bug Type/scope: A problem that needs fixing
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants