Repository: streamlink/streamlink

Issue:
plugins.twitch: TypeError: exception causes must derive from BaseException
### Checklist

- [X] This is a [plugin issue](https://streamlink.github.io/plugins.html) 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 plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

streamlink 6.2.0

### Description

I'm attempting to get a link for the free and publicly available stream of Thursday Night Football on Amazon's Twitch channel (twitch.tv/primevideo) to add to my Channels DVR so I can watch as if it was a traditional broadcast. It doesn't appear to matter if the channel is live or not as attempting to query offline channels simply returns a `No playable streams found on this URL` message whereas this causes a crash. 

### Debug log

```text
PS C:\Users\*user*> streamlink twitch.tv/primevideo best
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
Traceback (most recent call last):
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 894, in _get_hls_streams
    streams = TwitchHLSStream.parse_variant_playlist(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 710, in parse_variant_playlist
    res = cls._fetch_variant_playlist(session, url, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 666, in _fetch_variant_playlist
    res = session.http.get(url, exception=OSError, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\requests\sessions.py", line 602, in get
    return self.request("GET", url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\api\http_session.py", line 190, in request
    raise err from None  # TODO: fix this
    ^^^^^^^^^^^^^^^^^^^
OSError: Unable to open URL: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=358891&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=f73c97aef3f78f0a09540d793663adcf94cdacfe&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547649%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True (403 Client Error: Forbidden for url: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=358891&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=f73c97aef3f78f0a09540d793663adcf94cdacfe&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547649%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\bin\streamlink.exe\__main__.py", line 18, in <module>
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 929, in main
    handle_url()
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 551, in handle_url
    streams = fetch_streams(plugin)
              ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 449, in fetch_streams
    return plugin.streams(stream_types=args.stream_types,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\plugin.py", line 366, in streams
    ostreams = self._get_streams()
               ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 930, in _get_streams
    return self._get_hls_streams_live()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 875, in _get_hls_streams_live
    return self._get_hls_streams(url, restricted_bitrates)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 907, in _get_hls_streams
    raise PluginError(err) from err
TypeError: exception causes must derive from BaseException
PS C:\Users\*user*> streamlink twitch.tv/primevideo --disable-ads
usage: streamlink [OPTIONS] <URL> [STREAM]
streamlink: error: unrecognized arguments: --disable-ads
PS C:\Users\*user*> streamlink twitch.tv/primevideo --twitch-disable-ads
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
Traceback (most recent call last):
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 894, in _get_hls_streams
    streams = TwitchHLSStream.parse_variant_playlist(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 710, in parse_variant_playlist
    res = cls._fetch_variant_playlist(session, url, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 666, in _fetch_variant_playlist
    res = session.http.get(url, exception=OSError, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\requests\sessions.py", line 602, in get
    return self.request("GET", url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\api\http_session.py", line 190, in request
    raise err from None  # TODO: fix this
    ^^^^^^^^^^^^^^^^^^^
OSError: Unable to open URL: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=545759&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=3fafdeab499760891161dd22b67516af6b183667&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547741%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True (403 Client Error: Forbidden for url: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=545759&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=3fafdeab499760891161dd22b67516af6b183667&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547741%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
PS C:\Users\*user*> streamlink twitch.tv/primevideo best
[cli][info] Found matching plugin twitch for URL twitch.tv/primevideo
Traceback (most recent call last):
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 894, in _get_hls_streams
    streams = TwitchHLSStream.parse_variant_playlist(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 710, in parse_variant_playlist
    res = cls._fetch_variant_playlist(session, url, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\stream\hls.py", line 666, in _fetch_variant_playlist
    res = session.http.get(url, exception=OSError, **request_args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\requests\sessions.py", line 602, in get
    return self.request("GET", url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\api\http_session.py", line 190, in request
    raise err from None  # TODO: fix this
    ^^^^^^^^^^^^^^^^^^^
OSError: Unable to open URL: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=686255&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=d4cbfd5096874231459d870a473420927e28f7c3&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547814%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True (403 Client Error: Forbidden for url: https://usher.ttvnw.net/api/channel/hls/primevideo.m3u8?player=twitchweb&p=686255&type=any&allow_source=true&allow_audio_only=true&allow_spectre=false&sig=d4cbfd5096874231459d870a473420927e28f7c3&token=%7B%22adblock%22%3Afalse%2C%22authorization%22%3A%7B%22forbidden%22%3Afalse%2C%22reason%22%3A%22%22%7D%2C%22blackout_enabled%22%3Afalse%2C%22channel%22%3A%22primevideo%22%2C%22channel_id%22%3A168843586%2C%22chansub%22%3A%7B%22restricted_bitrates%22%3A%5B%5D%2C%22view_until%22%3A1924905600%7D%2C%22ci_gb%22%3Afalse%2C%22geoblock_reason%22%3A%22%22%2C%22device_id%22%3Anull%2C%22expires%22%3A1696547814%2C%22extended_history_allowed%22%3Afalse%2C%22game%22%3A%22%22%2C%22hide_ads%22%3Afalse%2C%22https_required%22%3Atrue%2C%22mature%22%3Afalse%2C%22partner%22%3Afalse%2C%22platform%22%3A%22web%22%2C%22player_type%22%3A%22embed%22%2C%22private%22%3A%7B%22allowed_to_view%22%3Atrue%7D%2C%22privileged%22%3Afalse%2C%22role%22%3A%22%22%2C%22server_ads%22%3Atrue%2C%22show_ads%22%3Atrue%2C%22subscriber%22%3Afalse%2C%22turbo%22%3Afalse%2C%22user_id%22%3Anull%2C%22user_ip%22%3A%2273.197.231.235%22%2C%22version%22%3A2%7D&fast_bread=True)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\bin\streamlink.exe\__main__.py", line 18, in <module>
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 929, in main
    handle_url()
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 551, in handle_url
    streams = fetch_streams(plugin)
              ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink_cli\main.py", line 449, in fetch_streams
    return plugin.streams(stream_types=args.stream_types,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugin\plugin.py", line 366, in streams
    ostreams = self._get_streams()
               ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 930, in _get_streams
    return self._get_hls_streams_live()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 875, in _get_hls_streams_live
    return self._get_hls_streams(url, restricted_bitrates)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\*user*\AppData\Local\Programs\Streamlink\pkgs\streamlink\plugins\twitch.py", line 907, in _get_hls_streams
    raise PluginError(err) from err
TypeError: exception causes must derive from BaseException
```



Discussion:
It's a 403 HTTP response on the HLS multivariant playlist request due to geo-restriction which isn't caught by the plugin (and its weird error handling). Let me fix this real quick...

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
@@ -17,12 +17,15 @@
 import logging
 import re
 import sys
+from contextlib import suppress
 from datetime import datetime, timedelta
 from json import dumps as json_dumps
 from random import random
 from typing import List, Mapping, NamedTuple, Optional, Tuple
 from urllib.parse import urlparse
 
+from requests.exceptions import HTTPError
+
 from streamlink.exceptions import NoStreamsError, PluginError
 from streamlink.plugin import Plugin, pluginargument, pluginmatcher
 from streamlink.plugin.api import validate
@@ -900,11 +903,22 @@ def _get_hls_streams(self, url, restricted_bitrates, **extra_params):
                 **extra_params,
             )
         except OSError as err:
-            err = str(err)
-            if "404 Client Error" in err or "Failed to parse playlist" in err:
+            # TODO: fix the "err" attribute set by HTTPSession.request()
+            orig = getattr(err, "err", None)
+            if isinstance(orig, HTTPError) and orig.response.status_code >= 400:
+                error = None
+                with suppress(PluginError):
+                    error = validate.Schema(
+                        validate.parse_json(),
+                        [{
+                            "type": "error",
+                            "error": str,
+                        }],
+                        validate.get((0, "error")),
+                    ).validate(orig.response.text)
+                log.error(error or "Could not access HLS playlist")
                 return
-            else:
-                raise PluginError(err) from err
+            raise PluginError(err) from err
 
         for name in restricted_bitrates:
             if name not in streams:


Test patch:
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -1,4 +1,5 @@
 import unittest
+from contextlib import nullcontext
 from datetime import datetime, timedelta, timezone
 from unittest.mock import MagicMock, Mock, call, patch
 
@@ -585,6 +586,60 @@ def test_failed_integrity_check(self, plugin: Twitch, mock: rm.Mocker):
         assert headers["Client-Integrity"] == "client-integrity-token"
 
 
+class TestTwitchHLSMultivariantResponse:
+    @pytest.fixture()
+    def plugin(self, request: pytest.FixtureRequest, requests_mock: rm.Mocker, session: Streamlink):
+        requests_mock.get("mock://multivariant", **getattr(request, "param", {}))
+        return Twitch(session, "https://twitch.tv/channelname")
+
+    @pytest.mark.parametrize(("plugin", "raises", "streams", "log"), [
+        pytest.param(
+            {"text": "#EXTM3U\n"},
+            nullcontext(),
+            {},
+            [],
+            id="success",
+        ),
+        pytest.param(
+            {"text": "Not an HLS playlist"},
+            pytest.raises(PluginError),
+            {},
+            [],
+            id="invalid HLS playlist",
+        ),
+        pytest.param(
+            {
+                "status_code": 403,
+                "json": [{
+                    "url": "mock://multivariant",
+                    "error": "Content Restricted In Region",
+                    "error_code": "content_geoblocked",
+                    "type": "error",
+                }],
+            },
+            nullcontext(),
+            None,
+            [("streamlink.plugins.twitch", "error", "Content Restricted In Region")],
+            id="geo restriction",
+        ),
+        pytest.param(
+            {
+                "status_code": 404,
+                "text": "Not found",
+            },
+            nullcontext(),
+            None,
+            [("streamlink.plugins.twitch", "error", "Could not access HLS playlist")],
+            id="non-json error response",
+        ),
+    ], indirect=["plugin"])
+    def test_multivariant_response(self, caplog: pytest.LogCaptureFixture, plugin: Twitch, raises, streams, log):
+        caplog.set_level("error", "streamlink.plugins.twitch")
+        with raises:
+            assert plugin._get_hls_streams("mock://multivariant", []) == streams
+        assert [(record.name, record.levelname, record.message) for record in caplog.records] == log
+
+
 class TestTwitchMetadata:
     @pytest.fixture()
     def metadata(self, request: pytest.FixtureRequest, session: Streamlink):