diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index 82f14ca968f266b..974bd41c15a38d3 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -361,9 +361,9 @@ def test_default_http_open(self): ) self.assertTrue(result) - def test_default_non_http_uses_bundle_id(self): - # Non-http(s) URLs (e.g. file://) must be routed through the browser - # via -b to prevent OS file handler dispatch. + def test_default_file_uses_bundle_id(self): + # file:// URLs must be routed through the browser via -b + # to prevent OS file handler dispatch. file_url = 'file:///tmp/test.html' browser = webbrowser.MacOS('default') with mock.patch('webbrowser._macos_default_browser_bundle_id', @@ -377,6 +377,23 @@ def test_default_non_http_uses_bundle_id(self): ) self.assertTrue(result) + def test_default_custom_scheme_uses_plain_open(self): + # Custom app URI schemes (e.g. vscode://, slack://) are not routed + # through the default browser's bundle ID -- the OS should resolve + # the scheme's own registered handler (gh-149454). + custom_url = 'vscode://vscode-remote/ssh-remote+host/workspace' + browser = webbrowser.MacOS('default') + with mock.patch('webbrowser._macos_default_browser_bundle_id') as mock_bundle, \ + mock.patch('subprocess.run') as mock_run: + mock_run.return_value = mock.Mock(returncode=0) + result = browser.open(custom_url) + mock_run.assert_called_once_with( + ['/usr/bin/open', custom_url], + stderr=subprocess.DEVNULL, + ) + mock_bundle.assert_not_called() + self.assertTrue(result) + def test_named_known_browser_uses_bundle_id(self): # Named browsers with a known bundle ID use /usr/bin/open -b. browser = webbrowser.MacOS('safari') diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index ec8b544a6b05235..372811e05bdfd81 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -651,11 +651,16 @@ class MacOS(BaseBrowser): For http/https URLs with the default browser, /usr/bin/open is called directly; macOS routes these to the registered browser. - For all other URL schemes (e.g. file://) and for named browsers, - /usr/bin/open -b is used so that the URL is always passed - to a browser application rather than dispatched by the OS file handler. - This prevents file injection attacks where a file:// URL pointing to an - executable bundle could otherwise be launched by the OS. + For file:// URLs and for named browsers, /usr/bin/open -b + is used so that the URL is always passed to a browser application + rather than dispatched by the OS file handler. This prevents file + injection attacks where a file:// URL pointing to an executable + bundle could otherwise be launched by the OS. + + Other URL schemes (e.g. custom app schemes like vscode:// or + slack://) are passed to /usr/bin/open without a bundle ID, so macOS + resolves the scheme's own registered handler application instead of + forcing it through the default browser. Named browsers with known bundle IDs use -b; unknown names fall back to -a. @@ -678,9 +683,19 @@ def open(self, url, new=0, autoraise=True): proto, sep, _ = url.partition(':') if sep and proto.lower() in {'http', 'https'}: cmd = ['/usr/bin/open', url] - else: + elif sep and proto.lower() == 'file': + # file:// URLs must be routed through the default + # browser explicitly: the OS's generic file handler + # dispatches file: URLs by the target file's type (e.g. + # a text editor for .html), not to a web browser. bundle_id = _macos_default_browser_bundle_id() cmd = ['/usr/bin/open', '-b', bundle_id, url] + else: + # Other custom URI schemes (e.g. vscode://, slack://) + # aren't web content: let the OS resolve the scheme's + # own registered handler instead of forcing it through + # the browser (gh-149454). + cmd = ['/usr/bin/open', url] else: bundle_id = self._BUNDLE_IDS.get(self.name.lower()) if bundle_id: diff --git a/Misc/NEWS.d/next/Library/2026-08-15-16-20-24.gh-issue-149454.YzcVmV.rst b/Misc/NEWS.d/next/Library/2026-08-15-16-20-24.gh-issue-149454.YzcVmV.rst new file mode 100644 index 000000000000000..a775e4676a3e0f0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-16-20-24.gh-issue-149454.YzcVmV.rst @@ -0,0 +1,7 @@ +On macOS, :func:`webbrowser.open` no longer forces custom (non-``file``, +non-``http``/``https``) URI schemes such as ``vscode://`` or ``slack://`` +through the default web browser. It now lets the OS resolve the scheme's +own registered handler application, restoring the behavior from Python +3.13 and earlier. This undoes an unintended side effect of the fix in +gh-130535, which addressed :func:`webbrowser.open` mishandling ``file://`` +URLs.