Repository: aaugustin/websockets
Pull request #393 (2018-05-12 17:31:24)

Improve handling of HTTP requests.
When receiving HTTP requests to WS endpoints:
- Avoid polluting logs at the warning level — but keep logs at the debug level because it must be possible to debug failures
- Return a HTTP 426 status code to plain HTTP requests


Patch:
diff --git a/websockets/compatibility.py b/websockets/compatibility.py
index c3bb613..21bc586 100644
--- a/websockets/compatibility.py
+++ b/websockets/compatibility.py
@@ -21,6 +21,7 @@ try:                                                # pragma: no cover
     BAD_REQUEST = http.HTTPStatus.BAD_REQUEST
     UNAUTHORIZED = http.HTTPStatus.UNAUTHORIZED
     FORBIDDEN = http.HTTPStatus.FORBIDDEN
+    UPGRADE_REQUIRED = http.HTTPStatus.UPGRADE_REQUIRED
     INTERNAL_SERVER_ERROR = http.HTTPStatus.INTERNAL_SERVER_ERROR
     SERVICE_UNAVAILABLE = http.HTTPStatus.SERVICE_UNAVAILABLE
 except AttributeError:                              # pragma: no cover
@@ -45,6 +46,10 @@ except AttributeError:                              # pragma: no cover
         value = 403
         phrase = "Forbidden"
 
+    class UPGRADE_REQUIRED:
+        value = 426
+        phrase = "Upgrade Required"
+
     class INTERNAL_SERVER_ERROR:
         value = 500
         phrase = "Internal Server Error"
diff --git a/websockets/exceptions.py b/websockets/exceptions.py
index 210aa40..b4f7dfc 100644
--- a/websockets/exceptions.py
+++ b/websockets/exceptions.py
@@ -1,9 +1,10 @@
 __all__ = [
-    'AbortHandshake', 'InvalidHandshake', 'InvalidHeader', 'InvalidMessage',
-    'InvalidOrigin', 'InvalidState', 'InvalidStatusCode', 'NegotiationError',
-    'InvalidParameterName', 'InvalidParameterValue', 'DuplicateParameter',
-    'InvalidURI', 'ConnectionClosed', 'PayloadTooBig',
-    'WebSocketProtocolError',
+    'AbortHandshake', 'ConnectionClosed', 'DuplicateParameter',
+    'InvalidHandshake', 'InvalidHeader', 'InvalidHeaderFormat',
+    'InvalidHeaderValue', 'InvalidMessage', 'InvalidOrigin',
+    'InvalidParameterName', 'InvalidParameterValue', 'InvalidState',
+    'InvalidStatusCode', 'InvalidUpgrade', 'InvalidURI', 'NegotiationError',
+    'PayloadTooBig', 'WebSocketProtocolError',
 ]
 
 
@@ -37,21 +38,48 @@ class InvalidMessage(InvalidHandshake):
 
 class InvalidHeader(InvalidHandshake):
     """
-    Exception raised when a HTTP header doesn't have the expected format.
+    Exception raised when a HTTP header doesn't have a valid format or value.
 
     """
-    def __init__(self, message, string, pos):
-        self.string = string
-        self.pos = pos
-        message = "{} at {} in {}".format(message, pos, string)
+    def __init__(self, name, value):
+        if value:
+            message = "Invalid {} header: {}".format(name, value)
+        else:
+            message = "Missing or empty {} header".format(name)
         super().__init__(message)
 
 
-class InvalidOrigin(InvalidHandshake):
+class InvalidHeaderFormat(InvalidHeader):
+    """
+    Exception raised when a Sec-WebSocket-* HTTP header cannot be parsed.
+
+    """
+    def __init__(self, name, error, string, pos):
+        error = "{} at {} in {}".format(error, pos, string)
+        super().__init__(name, error)
+
+
+class InvalidHeaderValue(InvalidHeader):
+    """
+    Exception raised when a Sec-WebSocket-* HTTP header has a wrong value.
+
+    """
+
+
+class InvalidUpgrade(InvalidHeader):
+    """
+    Exception raised when a Upgrade or Connection header isn't correct.
+
+    """
+
+
+class InvalidOrigin(InvalidHeader):
     """
-    Exception raised when the origin in a handshake request is forbidden.
+    Exception raised when the Origin header in a request isn't allowed.
 
     """
+    def __init__(self, origin):
+        super().__init__('Origin', origin)
 
 
 class InvalidStatusCode(InvalidHandshake):
diff --git a/websockets/handshake.py b/websockets/handshake.py
index b843250..142fef6 100644
--- a/websockets/handshake.py
+++ b/websockets/handshake.py
@@ -35,10 +35,11 @@ To open a connection, a client must:
 """
 
 import base64
+import binascii
 import hashlib
 import random
 
-from .exceptions import InvalidHandshake
+from .exceptions import InvalidHeaderValue, InvalidUpgrade
 
 
 __all__ = [
@@ -56,8 +57,8 @@ def build_request(set_header):
     Return the ``key`` which must be passed to :func:`check_response`.
 
     """
-    rand = bytes(random.getrandbits(8) for _ in range(16))
-    key = base64.b64encode(rand).decode()
+    raw_key = bytes(random.getrandbits(8) for _ in range(16))
+    key = base64.b64encode(raw_key).decode()
     set_header('Upgrade', 'websocket')
     set_header('Connection', 'Upgrade')
     set_header('Sec-WebSocket-Key', key)
@@ -81,18 +82,25 @@ def check_request(get_header):
     responsibility of the caller.
 
     """
+    if get_header('Upgrade').lower() != 'websocket':
+        raise InvalidUpgrade('Upgrade', get_header('Upgrade'))
+
+    if get_header('Connection').lower() != 'upgrade':
+        raise InvalidUpgrade('Connection', get_header('Connection'))
+
+    key = get_header('Sec-WebSocket-Key')
     try:
-        assert get_header('Upgrade').lower() == 'websocket'
-        assert any(
-            token.strip() == 'upgrade'
-            for token in get_header('Connection').lower().split(','))
-        key = get_header('Sec-WebSocket-Key')
-        assert len(base64.b64decode(key.encode(), validate=True)) == 16
-        assert get_header('Sec-WebSocket-Version') == '13'
-    except Exception as exc:
-        raise InvalidHandshake("Invalid request") from exc
-    else:
-        return key
+        raw_key = base64.b64decode(key.encode(), validate=True)
+    except binascii.Error:
+        raise InvalidHeaderValue('Sec-WebSocket-Key', key)
+    if len(raw_key) != 16:
+        raise InvalidHeaderValue('Sec-WebSocket-Key', key)
+
+    version = get_header('Sec-WebSocket-Version')
+    if version != '13':
+        raise InvalidHeaderValue('Sec-WebSocket-Version', version)
+
+    return key
 
 
 def build_response(set_header, key):
@@ -123,14 +131,15 @@ def check_response(get_header, key):
     the caller.
 
     """
-    try:
-        assert get_header('Upgrade').lower() == 'websocket'
-        assert any(
-            token.strip() == 'upgrade'
-            for token in get_header('Connection').lower().split(','))
-        assert get_header('Sec-WebSocket-Accept') == accept(key)
-    except Exception as exc:
-        raise InvalidHandshake("Invalid response") from exc
+    if get_header('Upgrade').lower() != 'websocket':
+        raise InvalidUpgrade('Upgrade', get_header('Upgrade'))
+
+    if get_header('Connection').lower() != 'upgrade':
+        raise InvalidUpgrade('Connection', get_header('Connection'))
+
+    if get_header('Sec-WebSocket-Accept') != accept(key):
+        raise InvalidHeaderValue(
+            'Sec-WebSocket-Accept', get_header('Sec-WebSocket-Accept'))
 
 
 def accept(key):
diff --git a/websockets/headers.py b/websockets/headers.py
index c5b228c..873984f 100644
--- a/websockets/headers.py
+++ b/websockets/headers.py
@@ -9,7 +9,7 @@ from :mod:`websockets.headers`.
 
 import re
 
-from .exceptions import InvalidHeader
+from .exceptions import InvalidHeaderFormat
 
 
 __all__ = [
@@ -54,18 +54,19 @@ def parse_OWS(string, pos):
 _token_re = re.compile(r'[-!#$%&\'*+.^_`|~0-9a-zA-Z]+')
 
 
-def parse_token(string, pos):
+def parse_token(string, pos, header_name):
     """
     Parse a token from ``string`` at the given position.
 
     Return the token value and the new position.
 
-    Raise :exc:`~websockets.exceptions.InvalidHeader` on invalid inputs.
+    Raise :exc:`~websockets.exceptions.InvalidHeaderFormat` on invalid inputs.
 
     """
     match = _token_re.match(string, pos)
     if match is None:
-        raise InvalidHeader("Expected token", string=string, pos=pos)
+        raise InvalidHeaderFormat(
+            header_name, "expected token", string=string, pos=pos)
     return match.group(), match.end()
 
 
@@ -76,46 +77,48 @@ _quoted_string_re = re.compile(
 _unquote_re = re.compile(r'\\([\x09\x20-\x7e\x80-\xff])')
 
 
-def parse_quoted_string(string, pos):
+def parse_quoted_string(string, pos, header_name):
     """
     Parse a quoted string from ``string`` at the given position.
 
     Return the unquoted value and the new position.
 
-    Raise :exc:`~websockets.exceptions.InvalidHeader` on invalid inputs.
+    Raise :exc:`~websockets.exceptions.InvalidHeaderFormat` on invalid inputs.
 
     """
     match = _quoted_string_re.match(string, pos)
     if match is None:
-        raise InvalidHeader("Expected quoted string", string=string, pos=pos)
+        raise InvalidHeaderFormat(
+            header_name, "expected quoted string", string=string, pos=pos)
     return _unquote_re.sub(r'\1', match.group()[1:-1]), match.end()
 
 
-def parse_extension_param(string, pos):
+def parse_extension_param(string, pos, header_name):
     """
     Parse a single extension parameter from ``string`` at the given position.
 
     Return a ``(name, value)`` pair and the new position.
 
-    Raise :exc:`~websockets.exceptions.InvalidHeader` on invalid inputs.
+    Raise :exc:`~websockets.exceptions.InvalidHeaderFormat` on invalid inputs.
 
     """
     # Extract parameter name.
-    name, pos = parse_token(string, pos)
+    name, pos = parse_token(string, pos, header_name)
     pos = parse_OWS(string, pos)
     # Extract parameter string, if there is one.
     if peek_ahead(string, pos) == '=':
         pos = parse_OWS(string, pos + 1)
         if peek_ahead(string, pos) == '"':
             pos_before = pos    # for proper error reporting below
-            value, pos = parse_quoted_string(string, pos)
+            value, pos = parse_quoted_string(string, pos, header_name)
             # https://tools.ietf.org/html/rfc6455#section-9.1 says: the value
             # after quoted-string unescaping MUST conform to the 'token' ABNF.
             if _token_re.fullmatch(value) is None:
-                raise InvalidHeader("Invalid quoted string content",
-                                    string=string, pos=pos_before)
+                raise InvalidHeaderFormat(
+                    header_name, "invalid quoted string content",
+                    string=string, pos=pos_before)
         else:
-            value, pos = parse_token(string, pos)
+            value, pos = parse_token(string, pos, header_name)
         pos = parse_OWS(string, pos)
     else:
         value = None
@@ -123,29 +126,30 @@ def parse_extension_param(string, pos):
     return (name, value), pos
 
 
-def parse_extension(string, pos):
+def parse_extension(string, pos, header_name):
     """
     Parse an extension definition from ``string`` at the given position.
 
     Return an ``(extension name, parameters)`` pair, where ``parameters`` is a
     list of ``(name, value)`` pairs, and the new position.
 
-    Raise :exc:`~websockets.exceptions.InvalidHeader` on invalid inputs.
+    Raise :exc:`~websockets.exceptions.InvalidHeaderFormat` on invalid inputs.
 
     """
     # Extract extension name.
-    name, pos = parse_token(string, pos)
+    name, pos = parse_token(string, pos, header_name)
     pos = parse_OWS(string, pos)
     # Extract all parameters.
     parameters = []
     while peek_ahead(string, pos) == ';':
         pos = parse_OWS(string, pos + 1)
-        parameter, pos = parse_extension_param(string, pos)
+        parameter, pos = parse_extension_param(string, pos, header_name)
         parameters.append(parameter)
     return (name, parameters), pos
 
 
-def parse_extension_list(string, pos=0):
+def parse_extension_list(
+        string, pos=0, header_name='Sec-WebSocket-Extensions'):
     """
     Parse a ``Sec-WebSocket-Extensions`` header.
 
@@ -166,7 +170,7 @@ def parse_extension_list(string, pos=0):
 
     Parameter values are ``None`` when no value is provided.
 
-    Raise :exc:`~websockets.exceptions.InvalidHeader` on invalid inputs.
+    Raise :exc:`~websockets.exceptions.InvalidHeaderFormat` on invalid inputs.
 
     """
     # Per https://tools.ietf.org/html/rfc7230#section-7, "a recipient MUST
@@ -180,7 +184,7 @@ def parse_extension_list(string, pos=0):
     extensions = []
     while True:
         # Loop invariant: an extension starts at pos in string.
-        extension, pos = parse_extension(string, pos)
+        extension, pos = parse_extension(string, pos, header_name)
         extensions.append(extension)
 
         # We may have reached the end of the string.
@@ -191,7 +195,8 @@ def parse_extension_list(string, pos=0):
         if peek_ahead(string, pos) == ',':
             pos = parse_OWS(string, pos + 1)
         else:
-            raise InvalidHeader("Expected comma", string=string, pos=pos)
+            raise InvalidHeaderFormat(
+                header_name, "expected comma", string=string, pos=pos)
 
         # Remove extra delimiters before the next extension.
         while peek_ahead(string, pos) == ',':
@@ -235,21 +240,21 @@ def build_extension_list(extensions):
     )
 
 
-def parse_protocol(string, pos):
+def parse_protocol(string, pos, header_name):
     """
     Parse a protocol definition from ``string`` at the given position.
 
     Return the protocol and the new position.
 
-    Raise :exc:`~websockets.exceptions.InvalidHeader` on invalid inputs.
+    Raise :exc:`~websockets.exceptions.InvalidHeaderFormat` on invalid inputs.
 
     """
-    name, pos = parse_token(string, pos)
+    name, pos = parse_token(string, pos, header_name)
     pos = parse_OWS(string, pos)
     return name, pos
 
 
-def parse_protocol_list(string, pos=0):
+def parse_protocol_list(string, pos=0, header_name='Sec-WebSocket-Protocol'):
     """
     Parse a ``Sec-WebSocket-Protocol`` header.
 
@@ -257,7 +262,7 @@ def parse_protocol_list(string, pos=0):
 
     Return a list of protocols.
 
-    Raise :exc:`~websockets.exceptions.InvalidHeader` on invalid inputs.
+    Raise :exc:`~websockets.exceptions.InvalidHeaderFormat` on invalid inputs.
 
     """
     # Per https://tools.ietf.org/html/rfc7230#section-7, "a recipient MUST
@@ -271,7 +276,7 @@ def parse_protocol_list(string, pos=0):
     protocols = []
     while True:
         # Loop invariant: a protocol starts at pos in string.
-        protocol, pos = parse_protocol(string, pos)
+        protocol, pos = parse_protocol(string, pos, header_name)
         protocols.append(protocol)
 
         # We may have reached the end of the string.
@@ -282,7 +287,8 @@ def parse_protocol_list(string, pos=0):
         if peek_ahead(string, pos) == ',':
             pos = parse_OWS(string, pos + 1)
         else:
-            raise InvalidHeader("Expected comma", string=string, pos=pos)
+            raise InvalidHeaderFormat(
+                header_name, "expected comma", string=string, pos=pos)
 
         # Remove extra delimiters before the next protocol.
         while peek_ahead(string, pos) == ',':
diff --git a/websockets/server.py b/websockets/server.py
index b265bdd..3a81041 100644
--- a/websockets/server.py
+++ b/websockets/server.py
@@ -10,11 +10,11 @@ import sys
 
 from .compatibility import (
     BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE,
-    SWITCHING_PROTOCOLS, asyncio_ensure_future
+    SWITCHING_PROTOCOLS, UPGRADE_REQUIRED, asyncio_ensure_future
 )
 from .exceptions import (
     AbortHandshake, InvalidHandshake, InvalidMessage, InvalidOrigin,
-    NegotiationError
+    InvalidUpgrade, NegotiationError
 )
 from .extensions.permessage_deflate import ServerPerMessageDeflateFactory
 from .handshake import build_response, check_request
@@ -96,7 +96,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
                     early_response = (
                         SERVICE_UNAVAILABLE,
                         [],
-                        b"Server is shutting down.",
+                        b"Server is shutting down.\n",
                     )
                 elif isinstance(exc, AbortHandshake):
                     early_response = (
@@ -105,25 +105,32 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
                         exc.body,
                     )
                 elif isinstance(exc, InvalidOrigin):
-                    logger.warning("Invalid origin", exc_info=True)
+                    logger.debug("Invalid origin", exc_info=True)
                     early_response = (
                         FORBIDDEN,
                         [],
-                        str(exc).encode(),
+                        (str(exc) + "\n").encode(),
+                    )
+                elif isinstance(exc, InvalidUpgrade):
+                    logger.debug("Invalid upgrade", exc_info=True)
+                    early_response = (
+                        UPGRADE_REQUIRED,
+                        [('Upgrade', 'websocket')],
+                        (str(exc) + "\n").encode(),
                     )
                 elif isinstance(exc, InvalidHandshake):
-                    logger.warning("Invalid handshake", exc_info=True)
+                    logger.debug("Invalid handshake", exc_info=True)
                     early_response = (
                         BAD_REQUEST,
                         [],
-                        str(exc).encode(),
+                        (str(exc) + "\n").encode(),
                     )
                 else:
                     logger.warning("Error in opening handshake", exc_info=True)
                     early_response = (
                         INTERNAL_SERVER_ERROR,
                         [],
-                        b"See server log for more information.",
+                        b"See server log for more information.\n",
                     )
 
                 yield from self.write_http_response(*early_response)
@@ -264,7 +271,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
         origin = get_header('Origin')
         if origins is not None:
             if origin not in origins:
-                raise InvalidOrigin("Origin not allowed: {}".format(origin))
+                raise InvalidOrigin(origin)
         return origin
 
     @staticmethod


Test patch:
diff --git a/websockets/test_client_server.py b/websockets/test_client_server.py
index ae30aff..67424df 100644
--- a/websockets/test_client_server.py
+++ b/websockets/test_client_server.py
@@ -10,6 +10,7 @@ import sys
 import tempfile
 import unittest
 import unittest.mock
+import urllib.error
 import urllib.request
 
 from .client import *
@@ -472,14 +473,10 @@ class ClientServerTests(unittest.TestCase):
         self.assertEqual(resp_headers.count("Server"), 1)
         self.assertIn("('Server', 'Eggs')", resp_headers)
 
-    @with_server(create_protocol=HealthCheckServerProtocol)
-    @with_client()
-    def test_custom_protocol_http_request(self):
-        # One URL returns an HTTP response.
-
-        # Set url to 'https?://<host>:<port>/__health__/'.
+    def make_http_request(self, path='/'):
+        # Set url to 'https?://<host>:<port><path>'.
         url = get_server_uri(
-            self.server, resource_name='/__health__/', secure=self.secure)
+            self.server, resource_name=path, secure=self.secure)
         url = url.replace('ws', 'http')
 
         if self.secure:
@@ -489,18 +486,42 @@ class ClientServerTests(unittest.TestCase):
             open_health_check = functools.partial(
                 urllib.request.urlopen, url)
 
+        return self.loop.run_in_executor(None, open_health_check)
+
+    @with_server(create_protocol=HealthCheckServerProtocol)
+    def test_http_request_http_endpoint(self):
+        # Making a HTTP request to a HTTP endpoint succeeds.
         response = self.loop.run_until_complete(
-            self.loop.run_in_executor(None, open_health_check))
+            self.make_http_request('/__health__/'))
 
         with contextlib.closing(response):
             self.assertEqual(response.code, 200)
             self.assertEqual(response.read(), b'status = green\n')
 
-        # Other URLs create a WebSocket connection.
+    @with_server(create_protocol=HealthCheckServerProtocol)
+    def test_http_request_ws_endpoint(self):
+        # Making a HTTP request to a WS endpoint fails.
+        with self.assertRaises(urllib.error.HTTPError) as raised:
+            self.loop.run_until_complete(self.make_http_request())
+
+        self.assertEqual(raised.exception.code, 426)
+        self.assertEqual(raised.exception.headers['Upgrade'], 'websocket')
 
+    @with_server(create_protocol=HealthCheckServerProtocol)
+    def test_ws_connection_http_endpoint(self):
+        # Making a WS connection to a HTTP endpoint fails.
+        with self.assertRaises(InvalidStatusCode) as raised:
+            self.start_client('/__health__/')
+
+        self.assertEqual(raised.exception.status_code, 200)
+
+    @with_server(create_protocol=HealthCheckServerProtocol)
+    def test_ws_connection_ws_endpoint(self):
+        # Making a WS connection to a WS endpoint succeeds.
+        self.start_client()
         self.loop.run_until_complete(self.client.send("Hello!"))
-        reply = self.loop.run_until_complete(self.client.recv())
-        self.assertEqual(reply, "Hello!")
+        self.loop.run_until_complete(self.client.recv())
+        self.stop_client()
 
     def assert_client_raises_code(self, status_code):
         with self.assertRaises(InvalidStatusCode) as raised:
diff --git a/websockets/test_exceptions.py b/websockets/test_exceptions.py
index 031490f..da87bed 100644
--- a/websockets/test_exceptions.py
+++ b/websockets/test_exceptions.py
@@ -20,12 +20,35 @@ class ExceptionsTests(unittest.TestCase):
                 "Malformed HTTP message",
             ),
             (
-                InvalidHeader("Expected token", "a=|", 3),
-                "Expected token at 3 in a=|",
+                InvalidHeader('Name', ''),
+                "Missing or empty Name header",
             ),
             (
-                InvalidOrigin("Origin not allowed: ''"),
-                "Origin not allowed: ''",
+                InvalidHeader('Name', 'Value'),
+                "Invalid Name header: Value",
+            ),
+            (
+                InvalidHeaderFormat(
+                    'Sec-WebSocket-Protocol', "expected token", 'a=|', 3),
+                "Invalid Sec-WebSocket-Protocol header: "
+                "expected token at 3 in a=|",
+            ),
+            (
+                InvalidHeaderValue('Sec-WebSocket-Version', '42'),
+                "Invalid Sec-WebSocket-Version header: 42",
+            ),
+
+            (
+                InvalidUpgrade('Upgrade', ''),
+                "Missing or empty Upgrade header",
+            ),
+            (
+                InvalidUpgrade('Connection', 'websocket'),
+                "Invalid Connection header: websocket",
+            ),
+            (
+                InvalidOrigin('http://bad.origin'),
+                'Invalid Origin header: http://bad.origin',
             ),
             (
                 InvalidStatusCode(403),
diff --git a/websockets/test_handshake.py b/websockets/test_handshake.py
index fee35bc..62b4ffc 100644
--- a/websockets/test_handshake.py
+++ b/websockets/test_handshake.py
@@ -1,3 +1,4 @@
+import collections
 import contextlib
 import unittest
 
@@ -31,7 +32,7 @@ class HandshakeTests(unittest.TestCase):
         Assert that the transformation made them invalid.
 
         """
-        headers = {}
+        headers = collections.defaultdict(lambda: '')
         build_request(headers.__setitem__)
         yield headers
         with self.assertRaises(InvalidHandshake):
@@ -85,7 +86,7 @@ class HandshakeTests(unittest.TestCase):
         Assert that the transformation made them invalid.
 
         """
-        headers = {}
+        headers = collections.defaultdict(lambda: '')
         build_response(headers.__setitem__, key)
         yield headers
         with self.assertRaises(InvalidHandshake):
diff --git a/websockets/test_headers.py b/websockets/test_headers.py
index 230aadf..efb41ae 100644
--- a/websockets/test_headers.py
+++ b/websockets/test_headers.py
@@ -1,6 +1,6 @@
 import unittest
 
-from .exceptions import InvalidHeader
+from .exceptions import InvalidHeaderFormat
 from .headers import *
 
 
@@ -66,7 +66,7 @@ class HeadersTests(unittest.TestCase):
             'foo; bar=" "',
         ]:
             with self.subTest(header=header):
-                with self.assertRaises(InvalidHeader):
+                with self.assertRaises(InvalidHeaderFormat):
                     parse_extension_list(header)
 
     def test_parse_protocol_list(self):
@@ -101,5 +101,5 @@ class HeadersTests(unittest.TestCase):
             'foo; bar',
         ]:
             with self.subTest(header=header):
-                with self.assertRaises(InvalidHeader):
+                with self.assertRaises(InvalidHeaderFormat):
                     parse_protocol_list(header)