Support for `file://` urls in webbrowser.open

Transferring from GitHub as suggested by @nad for discussion of a possible implementation.

Summary

Often, webbrowser.open will use a platform’s “default opener” such as open on mac, xdg-open on linux, or os.startfile on Windows. This works just fine for http[s] URLs, but when attempting to open file:// URLs, all of these mechanisms launch the default application by file-type, which is often not a browser.

As such, file:// URLs are effectively unsupported by webbrowser.open in many cases. though notably they do work reliably if $BROWSER is set to an actual browser executable.

There are APIs to lookup default browsers, but webbrowser.py does not use them (except sometimes on Linux, more below). It would be nice if webbrowser.open("file:///path/to/some.html") worked more often.

Proposal

I would like to introduce official support for file:// URLs by explicitly looking up the default browser and launching it, rather than using these indirect methods, which don’t handle file:// URLs consistently. Looking up default browsers is not always easy, but there are some standard APIs. This could be done always, or only for file:// URLs to limit the impact.

Prototype

I’ve written a prototype to test implementations. It uses the following per-platform APIs:

macOS

  1. lookup default browser Application with URLForApplicationToOpenURL via ctypes. This is almost identical to the existing ios implementation in webbrowser.py.
  2. launch it with applescript, just like existing applications

alternative: fully ctypes implementation, using openURLs:withApplication.... This would be even more similar to the existing ios implementation (maybe they could even be merged?)

Windows

  1. Lookup default browser via winreg ...\UrlAssociations\https\UserChoice
  2. Lookup command-line via $progId\shell\open\command
  3. replace Windows %1 with the %s webbrowser.py expects
  4. create a GenericBrowser with that command-line

(I have no idea how/when looking up these registry keys may fail on different Windows situations)

Linux / XDG

This one seems the hardest, since there are so many different situations to handle. But from my tests, I’ve found these ways to lookup the default browser:

xdg-settings get default-web-browser
# or 
xdg-mime query default x-scheme-handler/https

which returns e.g. firefox_firefox.desktop by default in a fresh Ubuntu install.

(notably, webbrowser.py does call xdg-settings get default-webbrowser, but this only sets the default browser if it is a strict match to a hardcoded list, which doesn’t include a default snap install of Firefox on Ubuntu because it is firefox_firefox.desktop not firefox.desktop).

Once the default browser is found, launch with either gtk-launch (preferred, since it searches $XDG_DATA paths), or gio launch (after searching $XDG_DATA, since gio launch seems to require absolute paths to .desktop files).

I have no idea how many other ways there are to lookup or launch ‘default’ browsers on various flavors of linux and other OSes.

alternative

since default-openers are known to not work for file:// URLs, they could explicitly not support them (return False from open), which would mean falling back on later choices that reference browsers explicitly. This would mean the default browser choice would not be respected (in the cases where it already isn’t), but at least a browser would be launched reliably instead of the default application, which may not be a browser at all.

5 Likes

I’ve commented on the issue about the macOS bits. It’s possible to to the browser detection in AppleScript, which makes it possible to integrate it into the code that’s already used on macOS for opening URLs.

This avoids introducing a dependency AppKit in webbrowser.open, in the past dependencies on system frameworks has proven to be problematic for code that uses os.fork to split of worker processes (without exec-ing a new binary).

3 Likes

Awesome, thanks! I think that means the mac part can be a PR without any trouble (unless there are things about invoking AppKit APIs in applescript I don’t know about). Does it make sense to do each platform as a separate PR? While it does happen to be true that all 3 have the same fundamental problem, the solutions are all different with different tradeoffs, and I guess might have different reviewers.

So slightly tweaked proposal:

  1. mac: add @ronaldoussoren’s applescript version of the AppKit lookup to detect to the existing ‘default’ branch
  2. linux: add gtk-launch as first priority if _os_preferred_browser is defined, since it allows launching a .desktop app by name
    a. also support gio launch, which requires XDG absolute path lookup of the .desktop file?
  3. windows: attempt winreg lookup before os.startfile in WindowsDefault

I also see a general design choice of whether to do this lookup always vs only for file:// urls (or non-http URLs), since the underlying call does already work for http URLs. Advantage of always doing it: consistent, simpler behavior. Advantage of only-for-file: minimal disruption if it ever fails, shorter code path in the 90% of cases that are actually http urls. I don’t know what folks would prefer.

Related to this, what would folks say is the expected behavior for ‘custom’ URL handlers, e.g. mailto: or iterm2://? Should they

  1. launch a webbrowser, as indicated by the module (most browsers, at least in my tests, prompt with an “open this in…” to launch the second application),
  2. launch the associated application (current default behavior most of the time)
  3. unsupported, undefined behavior (technically current behavior, since it depends on context and which opener is selected from the system)
1 Like

I’ve updated the prototype with the simpler applescript and with a structure that can more easily slot into a patch to webbrowser.py.

I chose to only look up the browser on mac and Windows if the URL is not http[s], minimizing the impact of the change but adding a branch.

and I’ve copied the prototype with suggested changes into a cpython branch ahead of making a PR. I’m not sure if splitting each platform into a separate PR would make it easier or harder to review, but if anyone has feedback in that regard, I can start opening the PR(s). In particular, I imagine the linux implementation (especially the priority order and disabling xdg-open support for file://) to prompt a bit more discussion than the other two.

1 Like

Does it make sense to do each platform as a separate PR?

I think it does since the different implementations will have different reviewers. (I don’t feel competent to detail review anything but the macOS part which is looking good to me.) And they should be able to be applied independently.

I also see a general design choice of whether to do this lookup always vs only for file:// urls (or non-http URLs)

I don’t have a strong opinion here but your choice to skip the lookup for http* seems reasonable.

expected behavior for ‘custom’ URL handlers

Again I don’t have a really strong opinion but, if we make these other changes, this would be the time to enforce the passing of the url to a webbrowser and document that as another change to previously undefined behavior (so either option 1 or do nothing = option 3).

1 Like

Great! I’ve opened 3 PRs

The mac one, at least, seems like a straightforward improvement. Windows is similar but less familiar to me so I have less confidence about caveats, and XDG has a lot more ambiguity and open questions.

It’s a little tricky for me to figure out where the docs changes go, since a generic “improved consistency across platforms” message doesn’t belong until/unless all three are merged.

1 Like