Repository: streamlink/streamlink

Issue:
tests: failure caused exceptions groups in test_webbrowser
### Checklist

- [X] This is a bug report and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

streamlink 6.7.2

### Description

While updating the Streamlink Debian package for Debian Stable, I got tests failure in `tests/webbrowser/test_webbrowser.py`.

The failure is from the exception group handling.

This code exception an exception like KeyboardInterrupt but what is raised is an BaseExceptionGroup with a single exception, the KeyboardInterrupt exception for example.
```python
with pytest.raises(exception):  # noqa: PT012
    async with webbrowser_launch() as (_nursery, process):
        assert process.poll() is None, "process is still running"
        raise exception()
```

I'm not sure where to fix this (in tests only or in the code that cause the BaseExceptionGroup to be raised), but wrapping `with catch(...)` calls with this fix the tests:

```python

        try:
            with catch(...):
                [...]
        # Unwrap single exception as done in more recent versions of python/exceptiongroup
        except BaseExceptionGroup as exc:
            if len(exc.exceptions) == 1:
                raise exc.exceptions[0]
            raise exc
```

The cause is that Debian Stable has exceptiongroup v1.1.0 which doesn't do this unwraping.
Installing exceptiongroup v1.2.0 using pip fixes the tests.

This was implemented in exceptiongroup from this commit: https://github.com/agronholm/exceptiongroup/commit/821d5ebaadfd0b5b16c785a942f69e89933217bf

I don't know if you really want to handle this case, but I'm interrested to know where it is best to handle this unwrapping ?
- In any code using `with catch(...)`
- In tests only so they don't fail

Thanks!

### Debug log

```text
============================= test session starts ==============================
platform linux -- Python 3.11.2, pytest-7.2.1, pluggy-1.0.0+repack
cachedir: /workspaces/streamlink-debian/.pytest_cache
rootdir: /workspaces/streamlink-debian/.pybuild/cpython3_3.11/build, configfile: ../../../pyproject.toml, testpaths: build_backend, tests
plugins: requests-mock-1.9.3, asyncio-0.20.3, trio-0.8.0
asyncio: mode=Mode.STRICT
collected 6887 items

tests/testutils/test_handshake.py ......                                 [  0%]
tests/utils/test_args.py ............................................... [  0%]
.....................                                                    [  1%]
tests/utils/test_cache.py .                                              [  1%]
tests/utils/test_crypto.py ..                                            [  1%]
tests/utils/test_data.py ......                                          [  1%]
tests/utils/test_formatter.py ...                                        [  1%]
tests/utils/test_l10n.py ............................................... [  2%]
.                                                                        [  2%]
tests/utils/test_module.py ...                                           [  2%]
tests/utils/test_named_pipe.py .......ssssss                             [  2%]
tests/utils/test_parse.py .......................                        [  2%]
tests/utils/test_path.py ...........                                     [  2%]
tests/utils/test_processoutput.py ........                               [  3%]
tests/utils/test_random.py .........                                     [  3%]
tests/utils/test_times.py .............................................. [  3%]
........................................................................ [  5%]
.........................................                                [  5%]
tests/utils/test_url.py ...................                              [  5%]
tests/session/test_http.py ............................                  [  6%]
tests/session/test_options.py ............................               [  6%]
tests/session/test_plugins.py ........................                   [  7%]
tests/session/test_session.py .............                              [  7%]
tests/test_api_validate.py ............................................. [  8%]
........................................................................ [  9%]
........................................................................ [ 10%]
...........                                                              [ 10%]
tests/test_api_websocket.py ..................                           [ 10%]
tests/test_buffers.py .................                                  [ 11%]
tests/test_cache.py .....................                                [ 11%]
tests/test_compat.py ......                                              [ 11%]
tests/test_logger.py ........................................            [ 12%]
tests/test_options.py ........................                           [ 12%]
tests/test_plugin.py ................................................... [ 13%]
..                                                                       [ 13%]
tests/test_plugin_userinput.py ....                                      [ 13%]
tests/test_streamlink_api.py .....                                       [ 13%]
tests/webbrowser/test_chromium.py ..................                     [ 13%]
tests/webbrowser/test_webbrowser.py ........FF...                        [ 14%]
tests/webbrowser/cdp/test_client.py .................................... [ 14%]
..........                                                               [ 14%]
tests/webbrowser/cdp/test_connection.py ..........................       [ 15%]
tests/stream/test_ffmpegmux.py ......................................... [ 15%]
                                                                         [ 15%]
tests/stream/test_file.py ..                                             [ 15%]
tests/stream/test_segmented.py .                                         [ 15%]
tests/stream/test_stream_json.py ........                                [ 16%]
tests/stream/test_stream_to_url.py ........                              [ 16%]
tests/stream/test_stream_wrappers.py .                                   [ 16%]
tests/stream/dash/test_dash.py ...........................               [ 16%]
tests/stream/dash/test_manifest.py ..................................... [ 17%]
....                                                                     [ 17%]
tests/stream/hls/test_hls.py ........................................... [ 17%]
.........                                                                [ 18%]
tests/stream/hls/test_hls_filtered.py ......                             [ 18%]
tests/stream/hls/test_m3u8.py .......................................... [ 18%]
........................                                                 [ 19%]
tests/test_plugins.py .................................................. [ 19%]
........................................................................ [ 21%]
........................................................................ [ 22%]
........................................................................ [ 23%]
........................................................................ [ 24%]
........................................................................ [ 25%]
........................................................................ [ 26%]
........................................................................ [ 27%]
........................................................................ [ 29%]
........................................................................ [ 30%]
........................................................................ [ 31%]
........................................................................ [ 32%]
........................................................................ [ 33%]
........................................................................ [ 34%]
........................................................................ [ 35%]
........................................................................ [ 37%]
........................................................................ [ 38%]
........................................................................ [ 39%]
........................................................................ [ 40%]
........................................................................ [ 41%]
........................................................................ [ 42%]
........................................................................ [ 43%]
........................................................................ [ 44%]
........................................................................ [ 46%]
........................................................................ [ 47%]
........................................................................ [ 48%]
........................................................................ [ 49%]
........................................................................ [ 50%]
........................................................................ [ 51%]
........................................................................ [ 52%]
..........................................................               [ 53%]
tests/plugins/test_abematv.py ..................                         [ 54%]
tests/plugins/test_adultswim.py ............                             [ 54%]
tests/plugins/test_afreeca.py ...............                            [ 54%]
tests/plugins/test_albavision.py ....................................... [ 55%]
.........                                                                [ 55%]
tests/plugins/test_aloula.py ...........................                 [ 55%]
tests/plugins/test_app17.py .........                                    [ 55%]
tests/plugins/test_ard_live.py .........                                 [ 56%]
tests/plugins/test_ard_mediathek.py ..................                   [ 56%]
tests/plugins/test_artetv.py ..............................              [ 56%]
tests/plugins/test_atpchallenger.py .........                            [ 56%]
tests/plugins/test_atresplayer.py ..............                         [ 57%]
tests/plugins/test_bbciplayer.py ..........                              [ 57%]
tests/plugins/test_bfmtv.py ...................                          [ 57%]
tests/plugins/test_bigo.py .......                                       [ 57%]
tests/plugins/test_bilibili.py ........                                  [ 57%]
tests/plugins/test_blazetv.py ...............                            [ 58%]
tests/plugins/test_bloomberg.py ..................                       [ 58%]
tests/plugins/test_booyah.py ..........................                  [ 58%]
tests/plugins/test_brightcove.py ........                                [ 58%]
tests/plugins/test_btv.py .........                                      [ 59%]
tests/plugins/test_cbsnews.py ....................                       [ 59%]
tests/plugins/test_cdnbg.py ................................             [ 59%]
tests/plugins/test_ceskatelevize.py ...................                  [ 60%]
tests/plugins/test_cinergroup.py .................                       [ 60%]
tests/plugins/test_clubbingtv.py ..........                              [ 60%]
tests/plugins/test_cmmedia.py ................                           [ 60%]
tests/plugins/test_cnews.py ..........                                   [ 60%]
tests/plugins/test_crunchyroll.py .................................      [ 61%]
tests/plugins/test_dailymotion.py .............                          [ 61%]
tests/plugins/test_dash.py ............................................. [ 62%]
..........................                                               [ 62%]
tests/plugins/test_delfi.py ...........                                  [ 62%]
tests/plugins/test_deutschewelle.py ...............                      [ 63%]
tests/plugins/test_dlive.py ......................                       [ 63%]
tests/plugins/test_dogan.py .........................                    [ 63%]
tests/plugins/test_dogus.py ..............                               [ 64%]
tests/plugins/test_drdk.py ............                                  [ 64%]
tests/plugins/test_earthcam.py ........                                  [ 64%]
tests/plugins/test_euronews.py .....................                     [ 64%]
tests/plugins/test_facebook.py ............                              [ 65%]
tests/plugins/test_filmon.py ..........................................  [ 65%]
tests/plugins/test_galatasaraytv.py ..........                           [ 65%]
tests/plugins/test_goltelevision.py ..............                       [ 66%]
tests/plugins/test_goodgame.py .................................         [ 66%]
tests/plugins/test_googledrive.py .......                                [ 66%]
tests/plugins/test_gulli.py .................                            [ 66%]
tests/plugins/test_hiplayer.py ........                                  [ 67%]
tests/plugins/test_hls.py .............................................. [ 67%]
................................................                         [ 68%]
tests/plugins/test_http.py .......................                       [ 68%]
tests/plugins/test_htv.py ........................                       [ 69%]
tests/plugins/test_huajiao.py ........                                   [ 69%]
tests/plugins/test_huya.py .........                                     [ 69%]
tests/plugins/test_idf1.py ............                                  [ 69%]
tests/plugins/test_indihometv.py .........                               [ 69%]
tests/plugins/test_invintus.py ...........                               [ 70%]
tests/plugins/test_kugou.py ............                                 [ 70%]
tests/plugins/test_linelive.py ...........                               [ 70%]
tests/plugins/test_livestream.py .............................           [ 70%]
tests/plugins/test_lnk.py ...............                                [ 71%]
tests/plugins/test_lrt.py ..............                                 [ 71%]
tests/plugins/test_ltv_lsm_lv.py .................                       [ 71%]
tests/plugins/test_mangomolo.py ............                             [ 71%]
tests/plugins/test_mdstrm.py ...............                             [ 72%]
tests/plugins/test_mediaklikk.py ..................                      [ 72%]
tests/plugins/test_mediavitrina.py ..................................... [ 72%]
..                                                                       [ 72%]
tests/plugins/test_mildom.py ..........                                  [ 73%]
tests/plugins/test_mitele.py ................                            [ 73%]
tests/plugins/test_mixcloud.py ....................                      [ 73%]
tests/plugins/test_mjunoon.py ...........                                [ 73%]
tests/plugins/test_mrtmk.py ...........                                  [ 74%]
tests/plugins/test_n13tv.py ..................                           [ 74%]
tests/plugins/test_nasaplus.py ........                                  [ 74%]
tests/plugins/test_nhkworld.py .......                                   [ 74%]
tests/plugins/test_nicolive.py ..............                            [ 74%]
tests/plugins/test_nimotv.py ...........                                 [ 74%]
tests/plugins/test_nos.py ................                               [ 75%]
tests/plugins/test_nownews.py ............                               [ 75%]
tests/plugins/test_nowtvtr.py ...........                                [ 75%]
tests/plugins/test_nrk.py ...................                            [ 75%]
tests/plugins/test_okru.py .............                                 [ 76%]
tests/plugins/test_olympicchannel.py ..................                  [ 76%]
tests/plugins/test_oneplusone.py ...............                         [ 76%]
tests/plugins/test_onetv.py .....................                        [ 76%]
tests/plugins/test_openrectv.py .........                                [ 77%]
tests/plugins/test_pandalive.py ........                                 [ 77%]
tests/plugins/test_piaulizaportal.py ............                        [ 77%]
tests/plugins/test_picarto.py ................                           [ 77%]
tests/plugins/test_piczel.py ..........                                  [ 77%]
tests/plugins/test_pixiv.py .........                                    [ 77%]
tests/plugins/test_pluto.py ............................................ [ 78%]
..........                                                               [ 78%]
tests/plugins/test_pluzz.py .................                            [ 79%]
tests/plugins/test_radiko.py ............                                [ 79%]
tests/plugins/test_radionet.py ..................                        [ 79%]
tests/plugins/test_raiplay.py ................                           [ 79%]
tests/plugins/test_reuters.py ............                               [ 79%]
tests/plugins/test_rtpa.py ........                                      [ 80%]
tests/plugins/test_rtpplay.py ..................                         [ 80%]
tests/plugins/test_rtve.py ..................                            [ 80%]
tests/plugins/test_rtvs.py ................                              [ 80%]
tests/plugins/test_ruv.py ....................                           [ 81%]
tests/plugins/test_sbscokr.py ..............                             [ 81%]
tests/plugins/test_showroom.py ............                              [ 81%]
tests/plugins/test_sportal.py .........                                  [ 81%]
tests/plugins/test_sportschau.py ........                                [ 81%]
tests/plugins/test_ssh101.py ...........                                 [ 82%]
tests/plugins/test_stadium.py ...........                                [ 82%]
tests/plugins/test_steam.py .............                                [ 82%]
tests/plugins/test_streamable.py .......                                 [ 82%]
tests/plugins/test_streann.py ................                           [ 82%]
tests/plugins/test_stv.py ........                                       [ 82%]
tests/plugins/test_svtplay.py .................                          [ 83%]
tests/plugins/test_swisstxt.py ..................                        [ 83%]
tests/plugins/test_telefe.py ..............                              [ 83%]
tests/plugins/test_telemadrid.py .........                               [ 83%]
tests/plugins/test_tf1.py ............................                   [ 84%]
tests/plugins/test_trovo.py ..............................               [ 84%]
tests/plugins/test_turkuvaz.py ................................          [ 85%]
tests/plugins/test_tv360.py .......                                      [ 85%]
tests/plugins/test_tv3cat.py .........................                   [ 85%]
tests/plugins/test_tv4play.py ..........                                 [ 85%]
tests/plugins/test_tv5monde.py .............                             [ 86%]
tests/plugins/test_tv8.py .......                                        [ 86%]
tests/plugins/test_tv999.py ............                                 [ 86%]
tests/plugins/test_tvibo.py .........                                    [ 86%]
tests/plugins/test_tviplayer.py ..........                               [ 86%]
tests/plugins/test_tvp.py .....................................          [ 87%]
tests/plugins/test_tvrby.py ............                                 [ 87%]
tests/plugins/test_tvrplus.py ...............                            [ 87%]
tests/plugins/test_tvtoya.py ..............                              [ 87%]
tests/plugins/test_twitcasting.py .......                                [ 88%]
tests/plugins/test_twitch.py ........................................... [ 88%]
............                                                             [ 88%]
tests/plugins/test_ustreamtv.py ...........................              [ 89%]
tests/plugins/test_ustvnow.py .........                                  [ 89%]
tests/plugins/test_vidio.py ..........                                   [ 89%]
tests/plugins/test_vimeo.py ..........................                   [ 90%]
tests/plugins/test_vinhlongtv.py ...........                             [ 90%]
tests/plugins/test_vk.py ..............................                  [ 90%]
tests/plugins/test_vkplay.py ...........                                 [ 90%]
tests/plugins/test_vtvgo.py ..................                           [ 91%]
tests/plugins/test_webtv.py .................                            [ 91%]
tests/plugins/test_welt.py ...........                                   [ 91%]
tests/plugins/test_wwenetwork.py ..........                              [ 91%]
tests/plugins/test_youtube.py .......................................... [ 92%]
.................                                                        [ 92%]
tests/plugins/test_yupptv.py ........                                    [ 92%]
tests/plugins/test_zattoo.py ......................................      [ 93%]
tests/plugins/test_zdf_mediathek.py .................                    [ 93%]
tests/plugins/test_zeenews.py ........                                   [ 93%]
tests/plugins/test_zengatv.py ............                               [ 94%]
tests/plugins/test_zhanqi.py .......                                     [ 94%]
tests/cli/test_argparser.py ...............................              [ 94%]
tests/cli/test_cmdline.py ....                                           [ 94%]
tests/cli/test_cmdline_title.py ....sssss                                [ 94%]
tests/cli/test_console.py ..............                                 [ 95%]
tests/cli/test_main.py .......................                           [ 95%]
tests/cli/test_main_check_file_output.py ......                          [ 95%]
tests/cli/test_main_formatter.py .........                               [ 95%]
tests/cli/test_main_logging.py ............................              [ 96%]
tests/cli/test_main_setup_config_args.py ............                    [ 96%]
tests/cli/test_plugin_args_and_options.py ...                            [ 96%]
tests/cli/test_streamrunner.py .....................                     [ 96%]
tests/cli/output/test_file.py ....s                                      [ 96%]
tests/cli/output/test_player.py ........s.......s.ss.....s.s............ [ 97%]
                                                                         [ 97%]
tests/cli/utils/test_formatter.py ...                                    [ 97%]
tests/cli/utils/test_path.py .................................s.s.s...s  [ 98%]
tests/cli/utils/test_player.py sssss.....                                [ 98%]
tests/cli/utils/test_progress.py ....................................... [ 98%]
......................................................s..                [ 99%]
tests/cli/utils/test_versioncheck.py ...............                     [100%]

=================================== FAILURES ===================================
____ TestLaunch.test_terminate_on_nursery_baseexception[KeyboardInterrupt] _____
  + Exception Group Traceback (most recent call last):
  |   File "/usr/lib/python3/dist-packages/trio/_core/_run.py", line 850, in __aexit__
  |     raise combined_error_from_nursery
  | trio.MultiError: BaseExceptionGroup('', [KeyboardInterrupt()])
  +-+---------------- 1 ----------------
    | Exception Group Traceback (most recent call last):
    |   File "/usr/lib/python3/dist-packages/pytest_trio/plugin.py", line 195, in _fixture_manager
    |     yield nursery_fixture
    |   File "/usr/lib/python3/dist-packages/pytest_trio/plugin.py", line 250, in run
    |     await self._func(**resolved_kwargs)
    |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/test_webbrowser.py", line 104, in test_terminate_on_nursery_baseexception
    |     async with webbrowser_launch() as (_nursery, process):
    |   File "/usr/lib/python3.11/contextlib.py", line 222, in __aexit__
    |     await self.gen.athrow(typ, value, traceback)
    |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/conftest.py", line 58, in webbrowser_launch
    |     async with webbrowser.launch(*args, **kwargs) as nursery:
    |   File "/usr/lib/python3.11/contextlib.py", line 222, in __aexit__
    |     await self.gen.athrow(typ, value, traceback)
    |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/streamlink/webbrowser/webbrowser.py", line 86, in launch
    |     with catch({  # type: ignore[dict-item]  # bug in exceptiongroup==1.2.0
    |   File "/usr/lib/python3/dist-packages/exceptiongroup/_catch.py", line 36, in __exit__
    |     raise unhandled from None
    | BaseExceptionGroup:  (1 sub-exception)
    +-+---------------- 1 ----------------
      | Traceback (most recent call last):
      |   File "/usr/lib/python3/dist-packages/exceptiongroup/_catch.py", line 52, in handle_exception
      |     handler(matched)
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/streamlink/webbrowser/webbrowser.py", line 84, in handle_baseexception
      |     raise exc_grp.exceptions[0] from exc_grp.exceptions[0].__context__
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/streamlink/webbrowser/webbrowser.py", line 106, in launch
      |     yield nursery
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/conftest.py", line 66, in webbrowser_launch
      |     yield nursery, process
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/test_webbrowser.py", line 106, in test_terminate_on_nursery_baseexception
      |     raise exception()
      | KeyboardInterrupt
      +------------------------------------
------------------------------ Captured log call -------------------------------
info     streamlink.webbrowser.webbrowser:webbrowser.py:90 Launching web browser: /usr/bin/python3.11
debug    streamlink.webbrowser.webbrowser:webbrowser.py:113 Waiting for web browser process to terminate
________ TestLaunch.test_terminate_on_nursery_baseexception[SystemExit] ________
  + Exception Group Traceback (most recent call last):
  |   File "/usr/lib/python3/dist-packages/trio/_core/_run.py", line 850, in __aexit__
  |     raise combined_error_from_nursery
  | trio.MultiError: BaseExceptionGroup('', [SystemExit()])
  +-+---------------- 1 ----------------
    | Exception Group Traceback (most recent call last):
    |   File "/usr/lib/python3/dist-packages/pytest_trio/plugin.py", line 195, in _fixture_manager
    |     yield nursery_fixture
    |   File "/usr/lib/python3/dist-packages/pytest_trio/plugin.py", line 250, in run
    |     await self._func(**resolved_kwargs)
    |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/test_webbrowser.py", line 104, in test_terminate_on_nursery_baseexception
    |     async with webbrowser_launch() as (_nursery, process):
    |   File "/usr/lib/python3.11/contextlib.py", line 222, in __aexit__
    |     await self.gen.athrow(typ, value, traceback)
    |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/conftest.py", line 58, in webbrowser_launch
    |     async with webbrowser.launch(*args, **kwargs) as nursery:
    |   File "/usr/lib/python3.11/contextlib.py", line 222, in __aexit__
    |     await self.gen.athrow(typ, value, traceback)
    |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/streamlink/webbrowser/webbrowser.py", line 86, in launch
    |     with catch({  # type: ignore[dict-item]  # bug in exceptiongroup==1.2.0
    |   File "/usr/lib/python3/dist-packages/exceptiongroup/_catch.py", line 36, in __exit__
    |     raise unhandled from None
    | BaseExceptionGroup:  (1 sub-exception)
    +-+---------------- 1 ----------------
      | Traceback (most recent call last):
      |   File "/usr/lib/python3/dist-packages/exceptiongroup/_catch.py", line 52, in handle_exception
      |     handler(matched)
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/streamlink/webbrowser/webbrowser.py", line 84, in handle_baseexception
      |     raise exc_grp.exceptions[0] from exc_grp.exceptions[0].__context__
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/streamlink/webbrowser/webbrowser.py", line 106, in launch
      |     yield nursery
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/conftest.py", line 66, in webbrowser_launch
      |     yield nursery, process
      |   File "/workspaces/streamlink-debian/.pybuild/cpython3_3.11/build/tests/webbrowser/test_webbrowser.py", line 106, in test_terminate_on_nursery_baseexception
      |     raise exception()
      | SystemExit
      +------------------------------------
------------------------------ Captured log call -------------------------------
info     streamlink.webbrowser.webbrowser:webbrowser.py:90 Launching web browser: /usr/bin/python3.11
debug    streamlink.webbrowser.webbrowser:webbrowser.py:113 Waiting for web browser process to terminate
=========================== short test summary info ============================
FAILED tests/webbrowser/test_webbrowser.py::TestLaunch::test_terminate_on_nursery_baseexception[KeyboardInterrupt]
FAILED tests/webbrowser/test_webbrowser.py::TestLaunch::test_terminate_on_nursery_baseexception[SystemExit]
================== 2 failed, 6309 passed, 28 skipped in 9.84s ==================
```



Discussion:
The `exceptiongroup<=1.1.1` incompatibility is annoying, but it can be fixed. This issue has revealed another issue though in regards to re-raising the `KeyboardInterrupt` and `SystemExit` exceptions.

Please have a look at #5930 if you can. Thanks for the report!

Btw, your pytest runner is missing the `build_backend` tests:
https://github.com/streamlink/streamlink/blob/f43d0eb4714de529a45dd9161e7774d070699bad/pyproject.toml#L126-L130

Resolving patch:
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -541,7 +541,7 @@ def acquire(
         headers: Mapping[str, str],
         device_id: str,
     ) -> Optional[Tuple[str, int]]:
-        from exceptiongroup import BaseExceptionGroup, catch  # noqa: PLC0415, I001
+        from exceptiongroup import BaseExceptionGroup  # noqa: PLC0415, I001
         from streamlink.webbrowser.cdp import CDPClient, CDPClientSession, devtools  # noqa: PLC0415
 
         url = f"https://www.twitch.tv/{channel}"
@@ -565,19 +565,17 @@ async def acquire_client_integrity_token(client: CDPClient):
                     await client_session.loaded(frame_id)
                     return await client_session.evaluate(js_get_integrity_token, timeout=eval_timeout)
 
-        def handle_error(exc_grp: BaseExceptionGroup) -> None:
-            for err in exc_grp.exceptions:
-                log.error(f"{type(err).__name__}: {err}")
-
-        with catch({  # type: ignore[dict-item]  # bug in exceptiongroup==1.2.0
-            Exception: handle_error,  # type: ignore[dict-item]  # bug in exceptiongroup==1.2.0
-        }):
+        try:
             client_integrity = CDPClient.launch(
                 session,
                 acquire_client_integrity_token,
                 # headless mode gets detected by Twitch, so we have to disable it regardless the user config
                 headless=False,
             )
+        except BaseExceptionGroup:
+            log.exception("Failed acquiring client integrity token")
+        except Exception as err:
+            log.error(err)
 
         if not client_integrity:
             return None
diff --git a/src/streamlink/webbrowser/webbrowser.py b/src/streamlink/webbrowser/webbrowser.py
--- a/src/streamlink/webbrowser/webbrowser.py
+++ b/src/streamlink/webbrowser/webbrowser.py
@@ -8,7 +8,7 @@
 from typing import AsyncContextManager, AsyncGenerator, Generator, List, Optional, Union
 
 import trio
-from exceptiongroup import BaseExceptionGroup, catch
+from exceptiongroup import BaseExceptionGroup
 
 from streamlink.utils.path import resolve_executable
 from streamlink.webbrowser.exceptions import WebbrowserError
@@ -80,12 +80,7 @@ def __init__(self, executable: Union[str, Path], arguments: List[str], timeout:
 
     @asynccontextmanager
     async def launch(self) -> AsyncGenerator[trio.Nursery, None]:
-        def handle_baseexception(exc_grp: BaseExceptionGroup) -> None:
-            raise exc_grp.exceptions[0] from exc_grp.exceptions[0].__context__
-
-        with catch({  # type: ignore[dict-item]  # bug in exceptiongroup==1.2.0
-            (KeyboardInterrupt, SystemExit): handle_baseexception,  # type: ignore[dict-item]  # bug in exceptiongroup==1.2.0
-        }):
+        try:
             async with trio.open_nursery() as nursery:
                 log.info(f"Launching web browser: {self.executable}")
                 # the process is run in a separate task
@@ -113,6 +108,13 @@ def handle_baseexception(exc_grp: BaseExceptionGroup) -> None:
                         log.debug("Waiting for web browser process to terminate")
                     # once the application logic is done, cancel the entire task group and terminate/kill the process
                     nursery.cancel_scope.cancel()
+        except BaseExceptionGroup as exc_grp:  # TODO: py310 support end: use except*
+            exc: Union[BaseException, BaseExceptionGroup, None] = exc_grp.subgroup((KeyboardInterrupt, SystemExit))
+            if not exc:  # not a KeyboardInterrupt or SystemExit
+                raise
+            while isinstance(exc, BaseExceptionGroup):  # get the first actual exception in the potentially nested groups
+                exc = exc.exceptions[0]
+            raise exc from exc.__context__
 
     async def _task_process_watcher(self, process: trio.Process, nursery: trio.Nursery) -> None:
         """Task for cancelling the launch task group if the user closes the browser or if it exits early on its own"""


Test patch:
diff --git a/tests/webbrowser/test_webbrowser.py b/tests/webbrowser/test_webbrowser.py
--- a/tests/webbrowser/test_webbrowser.py
+++ b/tests/webbrowser/test_webbrowser.py
@@ -7,6 +7,7 @@
 
 import pytest
 import trio
+from exceptiongroup import BaseExceptionGroup
 
 from streamlink.compat import is_win32
 from streamlink.webbrowser.exceptions import WebbrowserError
@@ -98,13 +99,30 @@ async def test_terminate_on_nursery_timeout(self, caplog: pytest.LogCaptureFixtu
 
     @pytest.mark.trio()
     @pytest.mark.parametrize("exception", [KeyboardInterrupt, SystemExit])
-    async def test_terminate_on_nursery_baseexception(self, caplog: pytest.LogCaptureFixture, webbrowser_launch, exception):
+    async def test_propagate_keyboardinterrupt_systemexit(self, caplog: pytest.LogCaptureFixture, webbrowser_launch, exception):
         process: trio.Process
-        with pytest.raises(exception):  # noqa: PT012
+        with pytest.raises(exception) as excinfo:  # noqa: PT012
             async with webbrowser_launch() as (_nursery, process):
                 assert process.poll() is None, "process is still running"
-                raise exception()
+                async with trio.open_nursery():
+                    raise exception()
 
+        assert isinstance(excinfo.value.__context__, BaseExceptionGroup)
+        assert process.poll() == (1 if is_win32 else -SIGTERM), "Process has been terminated"
+        assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
+            ("streamlink.webbrowser.webbrowser", "debug", "Waiting for web browser process to terminate"),
+        ]
+
+    @pytest.mark.trio()
+    async def test_terminate_on_exception(self, caplog: pytest.LogCaptureFixture, webbrowser_launch):
+        process: trio.Process
+        with pytest.raises(BaseExceptionGroup) as excinfo:  # noqa: PT012
+            async with webbrowser_launch() as (_nursery, process):
+                assert process.poll() is None, "process is still running"
+                async with trio.open_nursery():
+                    raise ZeroDivisionError()
+
+        assert excinfo.group_contains(ZeroDivisionError)
         assert process.poll() == (1 if is_win32 else -SIGTERM), "Process has been terminated"
         assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
             ("streamlink.webbrowser.webbrowser", "debug", "Waiting for web browser process to terminate"),