Repository: django/asgiref
Pull request #192 (2020-09-02 18:49:01)

Add TLS specification
Issue #112


Patch:
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index e4ed96f..2272802 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,9 +1,64 @@
-Pending
--------
+3.4.1 (2021-07-01)
+------------------
+
+* Fixed an issue with the deadlock detection where it had false positives
+  during exception handling.
+
+
+3.4.0 (2021-06-27)
+------------------
+
+* Calling sync_to_async directly from inside itself (which causes a deadlock
+  when in the default, thread-sensitive mode) now has deadlock detection.
+
+* asyncio usage has been updated to use the new versions of get_event_loop,
+  ensure_future, wait and gather, avoiding deprecation warnings in Python 3.10.
+  Python 3.6 installs continue to use the old versions; this is only for 3.7+
+
+* sync_to_async and async_to_sync now have improved type hints that pass
+  through the underlying function type correctly.
+
+* All Websocket* types are now spelled WebSocket, to match our specs and the
+  official spelling. The old names will work until release 3.5.0, but will
+  raise deprecation warnings.
+
+* The typing for WebSocketScope and HTTPScope's `extensions` key has been
+  fixed.
+
+
+3.3.4 (2021-04-06)
+------------------
+
+* The async_to_sync type error is now a warning due the high false negative
+  rate when trying to detect coroutine-returning callables in Python.
+
+
+3.3.3 (2021-04-06)
+------------------
+
+* The sync conversion functions now correctly detect functools.partial and other
+  wrappers around async functions on earlier Python releases.
+
+
+3.3.2 (2021-04-05)
+------------------
+
+* SyncToAsync now takes an optional "executor" argument if you want to supply
+  your own executor rather than using the built-in one.
 
 * async_to_sync and sync_to_async now check their arguments are functions of
   the correct type.
 
+* Raising CancelledError inside a SyncToAsync function no longer stops a future
+  call from functioning.
+
+* ThreadSensitive now provides context hooks/override options so it can be
+  made to be sensitive in a unit smaller than threads (e.g. per request)
+
+* Drop Python 3.5 support.
+
+* Add type annotations.
+
 3.3.1 (2020-11-09)
 ------------------
 
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..61d42bb
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,8 @@
+.PHONY: release clean
+
+clean:
+	rm -rf build/ dist/ asgiref.egg-info/
+
+release: clean
+	python3 -m build
+	python3 -m twine upload dist/*
diff --git a/README.rst b/README.rst
index f99aee6..fd13913 100644
--- a/README.rst
+++ b/README.rst
@@ -137,6 +137,18 @@ your documentation changes automatically::
     sphinx-autobuild . _build/html
 
 
+Releasing
+'''''''''
+
+To release, first add details to CHANGELOG.txt and update the version number in ``asgiref/__init__.py``.
+
+Then, build and push the packages::
+
+    python -m build
+    twine upload dist/*
+    rm -r build/ dist/
+
+
 Implementation Details
 ----------------------
 
@@ -177,7 +189,7 @@ This means you now have two basic states:
   runs on the main thread's event loop, and all ``thread_sensitive`` synchronous
   code will run in a single shared sub-thread.
 
-Cruicially, this means that in both cases there is a thread which is a shared
+Crucially, this means that in both cases there is a thread which is a shared
 resource that all ``thread_sensitive`` code must run on, and there is a chance
 that this thread is currently blocked on its own ``AsyncToSync`` call. Thus,
 ``AsyncToSync`` needs to act as an executor for thread code while it's blocking.
diff --git a/asgiref/__init__.py b/asgiref/__init__.py
index ff04168..a5cfdf5 100644
--- a/asgiref/__init__.py
+++ b/asgiref/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "3.3.1"
+__version__ = "3.4.1"
diff --git a/asgiref/_pep562.py b/asgiref/_pep562.py
new file mode 100644
index 0000000..3059257
--- /dev/null
+++ b/asgiref/_pep562.py
@@ -0,0 +1,61 @@
+"""
+Backport of PEP 562.
+https://pypi.org/search/?q=pep562
+Licensed under MIT
+Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions
+of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+"""
+import sys
+from typing import Any, Callable, List, Optional
+
+
+class Pep562:
+    """
+    Backport of PEP 562 <https://pypi.org/search/?q=pep562>.
+    Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`.
+    The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed.
+    """
+
+    def __init__(self, name: str) -> None:
+        """Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7."""
+
+        self._module = sys.modules[name]
+        self._get_attr = getattr(self._module, "__getattr__", None)
+        self._get_dir: Optional[Callable[..., List[str]]] = getattr(
+            self._module, "__dir__", None
+        )
+        sys.modules[name] = self  # type: ignore[assignment]
+
+    def __dir__(self) -> List[str]:
+        """Return the overridden `dir` if one was provided, else apply `dir` to the module."""
+
+        return self._get_dir() if self._get_dir else dir(self._module)
+
+    def __getattr__(self, name: str) -> Any:
+        """
+        Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present.
+        """
+
+        try:
+            return getattr(self._module, name)
+        except AttributeError:
+            if self._get_attr:
+                return self._get_attr(name)
+            raise
+
+
+def pep562(module_name: str) -> None:
+    """Helper function to apply PEP 562."""
+
+    if sys.version_info < (3, 7):
+        Pep562(module_name)
diff --git a/asgiref/compatibility.py b/asgiref/compatibility.py
index eccaee0..614b2e6 100644
--- a/asgiref/compatibility.py
+++ b/asgiref/compatibility.py
@@ -1,5 +1,6 @@
 import asyncio
 import inspect
+import sys
 
 
 def is_double_callable(application):
@@ -45,3 +46,16 @@ def guarantee_single_callable(application):
     if is_double_callable(application):
         application = double_to_single_callable(application)
     return application
+
+
+if sys.version_info >= (3, 7):
+    # these were introduced in 3.7
+    get_running_loop = asyncio.get_running_loop
+    run_future = asyncio.run
+    create_task = asyncio.create_task
+else:
+    # marked as deprecated in 3.10, did not exist before 3.7
+    get_running_loop = asyncio.get_event_loop
+    run_future = asyncio.ensure_future
+    # does nothing, this is fine for <3.7
+    create_task = lambda task: task
diff --git a/asgiref/server.py b/asgiref/server.py
index f975f78..fb1c394 100644
--- a/asgiref/server.py
+++ b/asgiref/server.py
@@ -3,7 +3,7 @@ import logging
 import time
 import traceback
 
-from .compatibility import guarantee_single_callable
+from .compatibility import get_running_loop, guarantee_single_callable, run_future
 
 logger = logging.getLogger(__name__)
 
@@ -56,7 +56,7 @@ class StatelessServer:
         """
         Runs the asyncio event loop with our handler loop.
         """
-        event_loop = asyncio.get_event_loop()
+        event_loop = get_running_loop()
         asyncio.ensure_future(self.application_checker())
         try:
             event_loop.run_until_complete(self.handle())
@@ -88,12 +88,12 @@ class StatelessServer:
         input_queue = asyncio.Queue()
         application_instance = guarantee_single_callable(self.application)
         # Run it, and stash the future for later checking
-        future = asyncio.ensure_future(
+        future = run_future(
             application_instance(
                 scope=scope,
                 receive=input_queue.get,
                 send=lambda message: self.application_send(scope, message),
-            )
+            ),
         )
         self.application_instances[scope_id] = {
             "input_queue": input_queue,
diff --git a/asgiref/sync.py b/asgiref/sync.py
index a8d91ea..0d9464f 100644
--- a/asgiref/sync.py
+++ b/asgiref/sync.py
@@ -1,12 +1,15 @@
 import asyncio.coroutines
 import functools
+import inspect
 import os
 import sys
 import threading
+import warnings
 import weakref
 from concurrent.futures import Future, ThreadPoolExecutor
-from typing import Dict
+from typing import Any, Callable, Dict, Optional, overload
 
+from .compatibility import get_running_loop
 from .current_thread_executor import CurrentThreadExecutor
 from .local import Local
 
@@ -27,6 +30,21 @@ def _restore_context(context):
             cvar.set(context.get(cvar))
 
 
+def _iscoroutinefunction_or_partial(func: Any) -> bool:
+    # Python < 3.8 does not correctly determine partially wrapped
+    # coroutine functions are coroutine functions, hence the need for
+    # this to exist. Code taken from CPython.
+    if sys.version_info >= (3, 8):
+        return asyncio.iscoroutinefunction(func)
+    else:
+        while inspect.ismethod(func):
+            func = func.__func__
+        while isinstance(func, functools.partial):
+            func = func.func
+
+        return asyncio.iscoroutinefunction(func)
+
+
 class ThreadSensitiveContext:
     """Async context manager to manage context for thread sensitive mode
 
@@ -101,8 +119,10 @@ class AsyncToSync:
     executors = Local()
 
     def __init__(self, awaitable, force_new_loop=False):
-        if not callable(awaitable) or not asyncio.iscoroutinefunction(awaitable):
-            raise TypeError("async_to_sync can only be applied to async functions.")
+        if not callable(awaitable) or not _iscoroutinefunction_or_partial(awaitable):
+            # Python does not have very reliable detection of async functions
+            # (lots of false negatives) so this is just a warning.
+            warnings.warn("async_to_sync was passed a non-async-marked callable")
         self.awaitable = awaitable
         try:
             self.__self__ = self.awaitable.__self__
@@ -113,7 +133,7 @@ class AsyncToSync:
             self.main_event_loop = None
         else:
             try:
-                self.main_event_loop = asyncio.get_event_loop()
+                self.main_event_loop = get_running_loop()
             except RuntimeError:
                 # There's no event loop in this thread. Look for the threadlocal if
                 # we're inside SyncToAsync
@@ -132,7 +152,7 @@ class AsyncToSync:
     def __call__(self, *args, **kwargs):
         # You can't call AsyncToSync from a thread with a running event loop
         try:
-            event_loop = asyncio.get_event_loop()
+            event_loop = get_running_loop()
         except RuntimeError:
             pass
         else:
@@ -219,7 +239,11 @@ class AsyncToSync:
                     tasks = asyncio.Task.all_tasks(loop)
                 for task in tasks:
                     task.cancel()
-                loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
+
+                async def gather():
+                    await asyncio.gather(*tasks, return_exceptions=True)
+
+                loop.run_until_complete(gather())
                 for task in tasks:
                     if task.cancelled():
                         continue
@@ -262,11 +286,11 @@ class AsyncToSync:
             if exc_info[1]:
                 try:
                     raise exc_info[1]
-                except Exception:
+                except BaseException:
                     result = await self.awaitable(*args, **kwargs)
             else:
                 result = await self.awaitable(*args, **kwargs)
-        except Exception as e:
+        except BaseException as e:
             call_result.set_exception(e)
         else:
             call_result.set_result(result)
@@ -293,11 +317,15 @@ class SyncToAsync:
     outermost), this will just be the main thread. This is achieved by idling
     with a CurrentThreadExecutor while AsyncToSync is blocking its sync parent,
     rather than just blocking.
+
+    If executor is passed in, that will be used instead of the loop's default executor.
+    In order to pass in an executor, thread_sensitive must be set to False, otherwise
+    a TypeError will be raised.
     """
 
     # If they've set ASGI_THREADS, update the default asyncio executor for now
     if "ASGI_THREADS" in os.environ:
-        loop = asyncio.get_event_loop()
+        loop = get_running_loop()
         loop.set_default_executor(
             ThreadPoolExecutor(max_workers=int(os.environ["ASGI_THREADS"]))
         )
@@ -320,26 +348,43 @@ class SyncToAsync:
     else:
         thread_sensitive_context: None = None
 
+    # Contextvar that is used to detect if the single thread executor
+    # would be awaited on while already being used in the same context
+    if sys.version_info >= (3, 7):
+        deadlock_context: "contextvars.ContextVar[bool]" = contextvars.ContextVar(
+            "deadlock_context"
+        )
+    else:
+        deadlock_context: None = None
+
     # Maintaining a weak reference to the context ensures that thread pools are
     # erased once the context goes out of scope. This terminates the thread pool.
     context_to_thread_executor: "weakref.WeakKeyDictionary[object, ThreadPoolExecutor]" = (
         weakref.WeakKeyDictionary()
     )
 
-    def __init__(self, func, thread_sensitive=True):
-        if not callable(func) or asyncio.iscoroutinefunction(func):
+    def __init__(
+        self,
+        func: Callable[..., Any],
+        thread_sensitive: bool = True,
+        executor: Optional["ThreadPoolExecutor"] = None,
+    ) -> None:
+        if not callable(func) or _iscoroutinefunction_or_partial(func):
             raise TypeError("sync_to_async can only be applied to sync functions.")
         self.func = func
         functools.update_wrapper(self, func)
         self._thread_sensitive = thread_sensitive
-        self._is_coroutine = asyncio.coroutines._is_coroutine
+        self._is_coroutine = asyncio.coroutines._is_coroutine  # type: ignore
+        if thread_sensitive and executor is not None:
+            raise TypeError("executor must not be set when thread_sensitive is True")
+        self._executor = executor
         try:
-            self.__self__ = func.__self__
+            self.__self__ = func.__self__  # type: ignore
         except AttributeError:
             pass
 
     async def __call__(self, *args, **kwargs):
-        loop = asyncio.get_event_loop()
+        loop = get_running_loop()
 
         # Work out what thread to run the code in
         if self._thread_sensitive:
@@ -360,11 +405,18 @@ class SyncToAsync:
                     # Create new thread executor in current context
                     executor = ThreadPoolExecutor(max_workers=1)
                     self.context_to_thread_executor[thread_sensitive_context] = executor
+            elif self.deadlock_context and self.deadlock_context.get(False):
+                raise RuntimeError(
+                    "Single thread executor already being used, would deadlock"
+                )
             else:
                 # Otherwise, we run it in a fixed single thread
                 executor = self.single_thread_executor
+                if self.deadlock_context:
+                    self.deadlock_context.set(True)
         else:
-            executor = None  # Use default
+            # Use the passed in executor, or the loop's default if it is None
+            executor = self._executor
 
         if contextvars is not None:
             context = contextvars.copy_context()
@@ -375,23 +427,27 @@ class SyncToAsync:
         else:
             func = self.func
 
-        # Run the code in the right thread
-        future = loop.run_in_executor(
-            executor,
-            functools.partial(
-                self.thread_handler,
-                loop,
-                self.get_current_task(),
-                sys.exc_info(),
-                func,
-                *args,
-                **kwargs,
-            ),
-        )
-        ret = await asyncio.wait_for(future, timeout=None)
+        try:
+            # Run the code in the right thread
+            future = loop.run_in_executor(
+                executor,
+                functools.partial(
+                    self.thread_handler,
+                    loop,
+                    self.get_current_task(),
+                    sys.exc_info(),
+                    func,
+                    *args,
+                    **kwargs,
+                ),
+            )
+            ret = await asyncio.wait_for(future, timeout=None)
 
-        if contextvars is not None:
-            _restore_context(context)
+        finally:
+            if contextvars is not None:
+                _restore_context(context)
+            if self.deadlock_context:
+                self.deadlock_context.set(False)
 
         return ret
 
@@ -424,7 +480,7 @@ class SyncToAsync:
             if exc_info[1]:
                 try:
                     raise exc_info[1]
-                except Exception:
+                except BaseException:
                     return func(*args, **kwargs)
             else:
                 return func(*args, **kwargs)
@@ -456,13 +512,37 @@ class SyncToAsync:
 async_to_sync = AsyncToSync
 
 
-def sync_to_async(func=None, thread_sensitive=True):
+@overload
+def sync_to_async(
+    func: None = None,
+    thread_sensitive: bool = True,
+    executor: Optional["ThreadPoolExecutor"] = None,
+) -> Callable[[Callable[..., Any]], SyncToAsync]:
+    ...
+
+
+@overload
+def sync_to_async(
+    func: Callable[..., Any],
+    thread_sensitive: bool = True,
+    executor: Optional["ThreadPoolExecutor"] = None,
+) -> SyncToAsync:
+    ...
+
+
+def sync_to_async(
+    func=None,
+    thread_sensitive=True,
+    executor=None,
+):
     if func is None:
         return lambda f: SyncToAsync(
             f,
             thread_sensitive=thread_sensitive,
+            executor=executor,
         )
     return SyncToAsync(
         func,
         thread_sensitive=thread_sensitive,
+        executor=executor,
     )
diff --git a/asgiref/typing.py b/asgiref/typing.py
index cc9181b..c8c2e78 100644
--- a/asgiref/typing.py
+++ b/asgiref/typing.py
@@ -1,11 +1,61 @@
 import sys
-from typing import Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union
+import warnings
+from typing import (
+    Any,
+    Awaitable,
+    Callable,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Tuple,
+    Type,
+    Union,
+)
+
+from asgiref._pep562 import pep562
 
 if sys.version_info >= (3, 8):
     from typing import Literal, Protocol, TypedDict
 else:
     from typing_extensions import Literal, Protocol, TypedDict
 
+__all__ = (
+    "ASGIVersions",
+    "HTTPScope",
+    "WebSocketScope",
+    "LifespanScope",
+    "WWWScope",
+    "Scope",
+    "HTTPRequestEvent",
+    "HTTPResponseStartEvent",
+    "HTTPResponseBodyEvent",
+    "HTTPServerPushEvent",
+    "HTTPDisconnectEvent",
+    "WebSocketConnectEvent",
+    "WebSocketAcceptEvent",
+    "WebSocketReceiveEvent",
+    "WebSocketSendEvent",
+    "WebSocketResponseStartEvent",
+    "WebSocketResponseBodyEvent",
+    "WebSocketDisconnectEvent",
+    "WebSocketCloseEvent",
+    "LifespanStartupEvent",
+    "LifespanShutdownEvent",
+    "LifespanStartupCompleteEvent",
+    "LifespanStartupFailedEvent",
+    "LifespanShutdownCompleteEvent",
+    "LifespanShutdownFailedEvent",
+    "ASGIReceiveEvent",
+    "ASGISendEvent",
+    "ASGIReceiveCallable",
+    "ASGISendCallable",
+    "ASGI2Protocol",
+    "ASGI2Application",
+    "ASGI3Application",
+    "ASGIApplication",
+)
+
 
 class ASGIVersions(TypedDict):
     spec_version: str
@@ -25,10 +75,10 @@ class HTTPScope(TypedDict):
     headers: Iterable[Tuple[bytes, bytes]]
     client: Optional[Tuple[str, int]]
     server: Optional[Tuple[str, Optional[int]]]
-    extensions: Dict[str, Dict[object, object]]
+    extensions: Optional[Dict[str, Dict[object, object]]]
 
 
-class WebsocketScope(TypedDict):
+class WebSocketScope(TypedDict):
     type: Literal["websocket"]
     asgi: ASGIVersions
     http_version: str
@@ -41,7 +91,7 @@ class WebsocketScope(TypedDict):
     client: Optional[Tuple[str, int]]
     server: Optional[Tuple[str, Optional[int]]]
     subprotocols: Iterable[str]
-    extensions: Dict[str, Dict[object, object]]
+    extensions: Optional[Dict[str, Dict[object, object]]]
 
 
 class LifespanScope(TypedDict):
@@ -49,8 +99,8 @@ class LifespanScope(TypedDict):
     asgi: ASGIVersions
 
 
-WWWScope = Union[HTTPScope, WebsocketScope]
-Scope = Union[HTTPScope, WebsocketScope, LifespanScope]
+WWWScope = Union[HTTPScope, WebSocketScope]
+Scope = Union[HTTPScope, WebSocketScope, LifespanScope]
 
 
 class HTTPRequestEvent(TypedDict):
@@ -81,46 +131,46 @@ class HTTPDisconnectEvent(TypedDict):
     type: Literal["http.disconnect"]
 
 
-class WebsocketConnectEvent(TypedDict):
+class WebSocketConnectEvent(TypedDict):
     type: Literal["websocket.connect"]
 
 
-class WebsocketAcceptEvent(TypedDict):
+class WebSocketAcceptEvent(TypedDict):
     type: Literal["websocket.accept"]
     subprotocol: Optional[str]
     headers: Iterable[Tuple[bytes, bytes]]
 
 
-class WebsocketReceiveEvent(TypedDict):
+class WebSocketReceiveEvent(TypedDict):
     type: Literal["websocket.receive"]
     bytes: Optional[bytes]
     text: Optional[str]
 
 
-class WebsocketSendEvent(TypedDict):
+class WebSocketSendEvent(TypedDict):
     type: Literal["websocket.send"]
     bytes: Optional[bytes]
     text: Optional[str]
 
 
-class WebsocketResponseStartEvent(TypedDict):
+class WebSocketResponseStartEvent(TypedDict):
     type: Literal["websocket.http.response.start"]
     status: int
     headers: Iterable[Tuple[bytes, bytes]]
 
 
-class WebsocketResponseBodyEvent(TypedDict):
+class WebSocketResponseBodyEvent(TypedDict):
     type: Literal["websocket.http.response.body"]
     body: bytes
     more_body: bool
 
 
-class WebsocketDisconnectEvent(TypedDict):
+class WebSocketDisconnectEvent(TypedDict):
     type: Literal["websocket.disconnect"]
     code: int
 
 
-class WebsocketCloseEvent(TypedDict):
+class WebSocketCloseEvent(TypedDict):
     type: Literal["websocket.close"]
     code: int
     reason: Optional[str]
@@ -155,9 +205,9 @@ class LifespanShutdownFailedEvent(TypedDict):
 ASGIReceiveEvent = Union[
     HTTPRequestEvent,
     HTTPDisconnectEvent,
-    WebsocketConnectEvent,
-    WebsocketReceiveEvent,
-    WebsocketDisconnectEvent,
+    WebSocketConnectEvent,
+    WebSocketReceiveEvent,
+    WebSocketDisconnectEvent,
     LifespanStartupEvent,
     LifespanShutdownEvent,
 ]
@@ -168,11 +218,11 @@ ASGISendEvent = Union[
     HTTPResponseBodyEvent,
     HTTPServerPushEvent,
     HTTPDisconnectEvent,
-    WebsocketAcceptEvent,
-    WebsocketSendEvent,
-    WebsocketResponseStartEvent,
-    WebsocketResponseBodyEvent,
-    WebsocketCloseEvent,
+    WebSocketAcceptEvent,
+    WebSocketSendEvent,
+    WebSocketResponseStartEvent,
+    WebSocketResponseBodyEvent,
+    WebSocketCloseEvent,
     LifespanStartupCompleteEvent,
     LifespanStartupFailedEvent,
     LifespanShutdownCompleteEvent,
@@ -204,3 +254,34 @@ ASGI3Application = Callable[
     Awaitable[None],
 ]
 ASGIApplication = Union[ASGI2Application, ASGI3Application]
+
+__deprecated__ = {
+    "WebsocketConnectEvent": WebSocketConnectEvent,
+    "WebsocketAcceptEvent": WebSocketAcceptEvent,
+    "WebsocketReceiveEvent": WebSocketReceiveEvent,
+    "WebsocketSendEvent": WebSocketSendEvent,
+    "WebsocketResponseStartEvent": WebSocketResponseStartEvent,
+    "WebsocketResponseBodyEvent": WebSocketResponseBodyEvent,
+    "WebsocketDisconnectEvent": WebSocketDisconnectEvent,
+    "WebsocketCloseEvent": WebSocketCloseEvent,
+}
+
+
+def __getattr__(name: str) -> Any:
+    deprecated = __deprecated__.get(name)
+    if deprecated:
+        stacklevel = 3 if sys.version_info >= (3, 7) else 4
+        warnings.warn(
+            f"'{name}' is deprecated. Use '{deprecated.__name__}' instead.",
+            category=DeprecationWarning,
+            stacklevel=stacklevel,
+        )
+        return deprecated
+    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
+def __dir__() -> List[str]:
+    return sorted(list(__all__) + list(__deprecated__.keys()))
+
+
+pep562(__name__)
diff --git a/docs/extensions.rst b/docs/extensions.rst
index aaf5764..766d840 100644
--- a/docs/extensions.rst
+++ b/docs/extensions.rst
@@ -128,3 +128,8 @@ Keys:
 After calling this extension to respond, the ASGI application itself should
 actively close the used file descriptor - ASGI servers are not responsible for
 closing descriptors.
+
+TLS
+---
+
+See :doc:`specs/tls`.
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index b86b795..d256327 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -12,3 +12,4 @@ the events that can be sent and received for each protocol.
    ASGI Specification <main>
    HTTP and WebSocket protocol <www>
    Lifespan <lifespan>
+   TLS Extension <tls>
diff --git a/docs/specs/tls.rst b/docs/specs/tls.rst
new file mode 100644
index 0000000..aff1c2f
--- /dev/null
+++ b/docs/specs/tls.rst
@@ -0,0 +1,1 @@
+.. include:: ../../specs/tls.rst
diff --git a/setup.cfg b/setup.cfg
index a3df063..01d0b50 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -49,7 +49,8 @@ ignore = E123,E128,E266,E402,W503,E731,W601
 max-line-length = 119
 
 [isort]
-line_length = 119
+profile = black
+multi_line_output = 3
 
 [mypy]
 warn_unused_ignores = True
diff --git a/specs/tls.rst b/specs/tls.rst
new file mode 100644
index 0000000..20908f8
--- /dev/null
+++ b/specs/tls.rst
@@ -0,0 +1,202 @@
+==================
+ASGI TLS Extension
+==================
+
+**Version**: 0.2 (2020-10-02)
+
+This specification outlines how to report TLS (or SSL) connection information
+in the ASGI *connection scope* object.
+
+The Base Protocol
+-----------------
+
+TLS is not usable on its own, it always wraps another protocol.
+So this specification is not designed to be usable on its own,
+it must be used as an extension to another ASGI specification.
+That other ASGI specification is referred to as the *base protocol
+specification*.
+
+For HTTP-over-TLS (HTTPS), use this TLS specification and the
+ASGI HTTP specification.  The *base protocol specification* is the
+ASGI HTTP specification.  (See :doc:`www`)
+
+For WebSockets-over-TLS (wss:// protocol), use this TLS specification
+and the ASGI WebSockets specification.  The *base protocol specification*
+is the ASGI WebSockets specification.  (See :doc:`www`)
+
+If using this extension with other protocols (not HTTPS or WebSockets), note
+that the *base protocol specification* must define the *connection scope* in a
+way that ensures it covers at most one TLS connection.  If not, you cannot use
+this extension.
+
+When to use this extension
+--------------------------
+
+This extension must only be used for TLS connections.
+
+For non-TLS connections, the ASGI server is forbidden from providing this
+extension.
+
+An ASGI application can check for the presence of the ``"tls"`` extension in
+the ``extensions`` dictionary in the connection scope.  If present, the server
+supports this extension and the connection is over TLS.  If not present,
+either the server does not support this extension or the connection is not
+over TLS.
+
+TLS Connection Scope
+--------------------
+
+The *connection scope* information passed in ``scope`` contains an
+``"extensions"`` key, which contains a dictionary of extensions.  Inside that
+dictionary, the key ``"tls"`` identifies the extension specified in this
+document.  The value will be a dictionary with the following entries:
+
+* ``server_cert`` (*Unicode string or None*) -- The PEM-encoded conversion
+  of the x509 certificate sent by the server when establishing the TLS
+  connection.  Some web server implementations may be unable to provide this
+  (e.g. if TLS is terminated by a separate proxy or load balancer); in that
+  case this shall be ``None``.  Mandatory.
+
+* ``client_cert_chain`` (*Iterable[Unicode string]*) -- An iterable of
+  Unicode strings, where each string is a PEM-encoded x509 certificate.
+  The first certificate is the client certificate.  Any subsequent certificates
+  are part of the certificate chain sent by the client, with each certificate
+  signing the preceding one.  If the client did not provide a client
+  certificate then it will be an empty iterable.  Some web server
+  implementations may be unable to provide this (e.g. if TLS is terminated by a
+  separate proxy or load balancer); in that case this shall be an empty
+  iterable.  Optional; if missing defaults to empty iterable.
+
+* ``client_cert_name`` (*Unicode string or None*) -- The x509 Distinguished
+  Name of the Subject of the client certificate, as a single string encoded as
+  defined in `RFC4514 <https://tools.ietf.org/html/rfc4514>`_.  If the client
+  did not provide a client certificate then it will be ``None``.  Some web
+  server implementations may be unable to provide this (e.g. if TLS is
+  terminated by a separate proxy or load balancer); in that case this shall be
+  ``None``. If ``client_cert_chain`` is provided and non-empty then this field
+  must be provided and must contain information that is consistent with
+  ``client_cert_chain[0]``.  Note that under some setups, (e.g. where TLS is
+  terminated by a separate proxy or load balancer and that device forwards the
+  client certificate name to the web server), this field may be set even where
+  ``client_cert_chain`` is not set.  Optional; if missing defaults to ``None``.
+
+* ``client_cert_error`` (*Unicode string or None*) -- ``None`` if a client
+  certificate was provided and successfully verified, or was not provided.
+  If a client certificate was provided but verification failed, this is a
+  non-empty string containing an error message or error code indicating why
+  validation failed; the details are web server specific.  Most web server
+  implementations will reject the connection if the client certificate
+  verification failed, instead of setting this value.  However, some may be
+  configured to allow the connection anyway.  This is especially useful when
+  testing that client certificates are supported properly by the client - it
+  allows a response containing an error message that can be presented to a
+  human, instead of just refusing the connection.  Optional; if missing defaults
+  to ``None``.
+
+* ``tls_version`` (*integer or None*) -- The TLS version in use.  This is one of
+  the version numbers as defined in the TLS specifications, which is an
+  unsigned integer.  Common values include ``0x0303`` for TLS 1.2 or ``0x0304``
+  for TLS 1.3.  If TLS is not in use, set to ``None``.  Some web server
+  implementations may be unable to provide this (e.g. if TLS is terminated by a
+  separate proxy or load balancer); in that case set to ``None``.  Mandatory.
+
+* ``cipher_suite`` (*integer or None*) -- The TLS cipher suite that is being
+  used.  This is a 16-bit unsigned integer that encodes the pair of 8-bit
+  integers specified in the relevant RFC, in network byte order.  For example
+  `RFC8446 section B.4 <https://tools.ietf.org/html/rfc8446#appendix-B.4>`_
+  defines that the cipher suite ``TLS_AES_128_GCM_SHA256`` is ``{0x13, 0x01}``;
+  that is encoded as a ``cipher_suite`` value of ``0x1301`` (equal to 4865
+  decimal).  Some web server implementations may be unable to provide this
+  (e.g. if TLS is terminated by a separate proxy or load balancer); in that case
+  set to ``None``.  Mandatory.
+
+Events
+------
+
+All events are as defined in the *base protocol specification*.
+
+Rationale (Informative)
+-----------------------
+
+This section explains the choices that led to this specification.
+
+Providing the entire TLS certificates in ``client_cert_chain``, rather than a
+parsed subset:
+
+* Makes it easier for web servers to implement, as they do not have to
+  include a parser for the entirety of the x509 certificate specifications
+  (which are huge and complicated).  They just have to convert the binary
+  DER format certificate from the wire, to the text PEM format.  That is
+  supported by many off-the-shelf libraries.
+* Makes it easier for web servers to maintain, as they do not have to update
+  their parser when new certificate fields are defined.
+* Makes it easier for clients as there are plenty of existing x509 libraries
+  available that they can use to parse the certificate; they don't need to
+  do some special ASGI-specific thing.
+* Improves interoperability as this is a simple, well-defined encoding, that
+  clients and servers are unlikely to get wrong.
+* Makes it much easier to write this specification.  There is no standard
+  documented format for a parsed certificate in Python, and we would need to
+  write one.
+* Makes it much easier to maintain this specification.  There is no need
+  to update a parsed certificate specification when new certificate fields
+  are defined.
+* Allows the client to support new certificate fields without requiring
+  any server changes, so long as the fields are marked as "non-critical" in
+  the certificate.  (A x509 parser is allowed to ignore non-critical fields
+  it does not understand.  Critical fields that are not understood cause
+  certificate parsing to fail).
+* Allows the client to do weird and wonderful things with the raw certificate,
+  instead of placing arbitrary limits on it.
+
+Specifying ``tls_version`` as an integer, not a string or float:
+
+* Avoids maintenance effort in this specification.  If a new version of TLS is
+  defined, then no changes are needed in this specification.
+* Does not significantly affect servers.  Whatever format we specified, servers
+  would likely need a lookup table from what their TLS library reports to what
+  this API needs.  (Unless their TLS library provides access to the raw value,
+  in which case it can be reported via this API directly).
+* Does not significantly affect clients.  Whatever format we specified, clients
+  would likely need a lookup table from what this API reports to the values
+  they support and wish to use internally.
+
+Specifying ``cipher_suite`` as an integer, not a string:
+
+* Avoids significant effort to compile a list of cipher suites in this
+  specification.  There are a huge number of existing TLS cipher suites, many
+  of which are not widely used, even listing them all would be a huge effort.
+* Avoids maintenance effort in this specification.  If a new cipher suite is
+  defined, then no changes are needed in this specification.
+* Avoids dependencies on nonstandard TLS-library-specific names.  E.g. the
+  cipher names used by OpenSSL are different from the cipher names used by the
+  RFCs.
+* Does not significantly affect servers.  Whatever format we specified, (unless
+  it was a nonstandard library-specific name and the server happened to use
+  that library), servers would likely need a lookup table from what their
+  TLS library reports to what this API needs.  (Unless their TLS library
+  provides access to the raw value, in which case it can be reported via this
+  API directly).
+* Does not significantly affect clients.  Whatever format we specified, clients
+  would likely need a lookup table from what this API reports to the values
+  they support and wish to use internally.
+* Using a single integer, rather than a pair of integers, makes handling this
+  value simpler and faster.
+
+``client_cert_name`` duplicates information that is also available in
+``client_cert_chain``.  However, many ASGI applications will probably find
+that information is sufficient for their application - it provides a simple
+string that identifies the user.  It is simpler to use than parsing the x509
+certificate.  For the server, this information is readily available.
+
+There are theoretical interoperability problems with ``client_cert_name``,
+since it depends on a list of object ID names that is maintained by IANA and
+theoretically can change.  In practice, this is not a real problem, since the
+object IDs that are actually used in certificates have not changed in many
+years.  So in practice it will be fine.
+
+
+Copyright
+---------
+
+This document has been placed in the public domain.
diff --git a/specs/www.rst b/specs/www.rst
index 99aa8cc..3d47efd 100644
--- a/specs/www.rst
+++ b/specs/www.rst
@@ -104,7 +104,8 @@ The *connection scope* information passed in ``scope`` contains:
   ``value`` is the header value. Order of header values must be preserved from
   the original HTTP request; order of header names is not important. Duplicates
   are possible and must be preserved in the message as received. Header names
-  must be lowercased. Pseudo headers (present in HTTP/2 and HTTP/3) must be
+  should be lowercased, but it is not required; servers should preserve header case
+  on a best-effort basis. Pseudo headers (present in HTTP/2 and HTTP/3) must be
   removed; if ``:authority`` is present its value must be added to the start of
   the iterable with ``host`` as the header name or replace any existing host
   header already present.
@@ -281,9 +282,11 @@ metadata (mostly from the HTTP request line and headers):
   ``[name, value]`` two-item iterables, where ``name`` is the header name and
   ``value`` is the header value. Order should be preserved from the original
   HTTP request; duplicates are possible and must be preserved in the message
-  as received. Header names must be lowercased. Pseudo headers (present in
-  HTTP/2 and HTTP/3) must be removed; if ``:authority`` is present its value
-  must be added to the start of the iterable with ``host`` as the header name
+  as received. Header names should be lowercased, but it is not required;
+  servers should preserve header case on a best-effort basis.
+  Pseudo headers (present in HTTP/2 and HTTP/3) must be removed;
+  if ``:authority`` is present its value must be added to the
+  start of the iterable with ``host`` as the header name
   or replace any existing host header already present.
 
 * ``client`` (*Iterable[Unicode string, int]*) -- A two-item iterable
diff --git a/tox.ini b/tox.ini
index 090987a..b3be3e4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py{36,37,38,39}-{test,mypy}
+    py{36,37,38,39,310}-{test,mypy}
     qa
 
 [testenv]
@@ -11,7 +11,7 @@ commands =
     mypy: mypy . {posargs}
 
 [testenv:qa]
-skip_install=true
+skip_install = true
 deps =
     pre-commit
 commands =


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 3bc8d80..925a488 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -3,7 +3,7 @@ name: Tests
 on:
   push:
    branches:
-   - master
+   - main
   pull_request:
 
 jobs:
@@ -21,17 +21,19 @@ jobs:
 
     steps:
       - uses: actions/checkout@v2
+
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python-version }}
+
       - name: Install dependencies
         run: |
-          python -m pip install --upgrade pip wheel setuptools tox
+          python -m pip install --upgrade pip setuptools wheel
+          python -m pip install --upgrade tox tox-py
+
       - name: Run tox targets for ${{ matrix.python-version }}
-        run: |
-          ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}")
-          TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox
+        run: tox --py current
 
   lint:
     name: Lint
diff --git a/tests/test_deprecated_types.py b/tests/test_deprecated_types.py
new file mode 100644
index 0000000..0adacff
--- /dev/null
+++ b/tests/test_deprecated_types.py
@@ -0,0 +1,20 @@
+import importlib
+
+import pytest
+
+from asgiref import typing
+
+
+@pytest.mark.parametrize("deprecated_type", typing.__deprecated__.keys())
+def test_deprecated_types(deprecated_type: str) -> None:
+    with pytest.warns(DeprecationWarning) as record:
+        getattr(importlib.import_module("asgiref.typing"), deprecated_type)
+        assert len(record) == 1
+        assert deprecated_type in str(record.list[0])
+
+
+@pytest.mark.parametrize("available_type", typing.__all__)
+def test_available_types(available_type: str) -> None:
+    with pytest.warns(None) as record:
+        getattr(importlib.import_module("asgiref.typing"), available_type)
+        assert len(record) == 0
diff --git a/tests/test_sync.py b/tests/test_sync.py
index b0882a1..254e8c9 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -1,5 +1,7 @@
 import asyncio
+import functools
 import multiprocessing
+import sys
 import threading
 import time
 from concurrent.futures import ThreadPoolExecutor
@@ -8,6 +10,7 @@ from unittest import TestCase
 
 import pytest
 
+from asgiref.compatibility import create_task, get_running_loop
 from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async
 
 
@@ -32,12 +35,17 @@ async def test_sync_to_async():
     assert result == 42
     assert end - start >= 1
     # Set workers to 1, call it twice and make sure that works right
-    loop = asyncio.get_event_loop()
-    old_executor = loop._default_executor
+    loop = get_running_loop()
+    old_executor = loop._default_executor or ThreadPoolExecutor()
     loop.set_default_executor(ThreadPoolExecutor(max_workers=1))
     try:
         start = time.monotonic()
-        await asyncio.wait([async_function(), async_function()])
+        await asyncio.wait(
+            [
+                create_task(async_function()),
+                create_task(async_function()),
+            ]
+        )
         end = time.monotonic()
         # It should take at least 2 seconds as there's only one worker.
         assert end - start >= 2
@@ -73,6 +81,24 @@ async def test_sync_to_async_fail_async():
     )
 
 
+@pytest.mark.asyncio
+async def test_async_to_sync_fail_partial():
+    """
+    sync_to_async raises a TypeError when applied to a sync partial.
+    """
+    with pytest.raises(TypeError) as excinfo:
+
+        async def test_function(*args):
+            pass
+
+        partial_function = functools.partial(test_function, 42)
+        sync_to_async(partial_function)
+
+    assert excinfo.value.args == (
+        "sync_to_async can only be applied to sync functions.",
+    )
+
+
 @pytest.mark.asyncio
 async def test_sync_to_async_decorator():
     """
@@ -184,11 +210,11 @@ def test_async_to_sync_fail_non_function():
     """
     async_to_sync raises a TypeError when applied to a non-function.
     """
-    with pytest.raises(TypeError) as excinfo:
+    with pytest.warns(UserWarning) as warnings:
         async_to_sync(1)
 
-    assert excinfo.value.args == (
-        "async_to_sync can only be applied to async functions.",
+    assert warnings[0].message.args == (
+        "async_to_sync was passed a non-async-marked callable",
     )
 
 
@@ -196,14 +222,14 @@ def test_async_to_sync_fail_sync():
     """
     async_to_sync raises a TypeError when applied to a sync function.
     """
-    with pytest.raises(TypeError) as excinfo:
+    with pytest.warns(UserWarning) as warnings:
 
         @async_to_sync
         def test_function(self):
             pass
 
-    assert excinfo.value.args == (
-        "async_to_sync can only be applied to async functions.",
+    assert warnings[0].message.args == (
+        "async_to_sync was passed a non-async-marked callable",
     )
 
 
@@ -319,6 +345,27 @@ def test_async_to_sync_in_except():
         assert test_function() == 42
 
 
+def test_async_to_sync_partial():
+    """
+    Tests we can call async_to_sync on an async partial.
+    """
+    result = {}
+
+    # Define async function
+    async def inner_async_function(*args):
+        await asyncio.sleep(0)
+        result["worked"] = True
+        return [*args]
+
+    partial_function = functools.partial(inner_async_function, 42)
+
+    # Run it
+    sync_function = async_to_sync(partial_function)
+    out = sync_function(84)
+    assert out == [42, 84]
+    assert result["worked"]
+
+
 def test_async_to_async_method_self_attribute():
     """
     Tests async_to_async on a method copies __self__.
@@ -388,7 +435,7 @@ async def test_thread_sensitive_outside_async():
         result["thread"] = threading.current_thread()
 
     # Run it (in supposed parallel!)
-    await asyncio.wait([outer(result_1), inner(result_2)])
+    await asyncio.wait([create_task(outer(result_1)), create_task(inner(result_2))])
 
     # They should not have run in the main thread, but in the same thread
     assert result_1["thread"] != threading.current_thread()
@@ -409,7 +456,10 @@ async def test_thread_sensitive_with_context_matches():
         async with ThreadSensitiveContext():
             # Run it (in supposed parallel!)
             await asyncio.wait(
-                [store_thread_async(result_1), store_thread_async(result_2)]
+                [
+                    create_task(store_thread_async(result_1)),
+                    create_task(store_thread_async(result_2)),
+                ]
             )
 
     await fn()
@@ -583,3 +633,87 @@ async def test_multiprocessing():
         return test_queue.get(True, 1)
 
     assert await sync_to_async(fork_first)() == 42
+
+
+@pytest.mark.asyncio
+async def test_sync_to_async_uses_executor():
+    """
+    Tests that SyncToAsync uses the passed in executor correctly.
+    """
+
+    class CustomExecutor:
+        def __init__(self):
+            self.executor = ThreadPoolExecutor(max_workers=1)
+            self.times_submit_called = 0
+
+        def submit(self, callable_, *args, **kwargs):
+            self.times_submit_called += 1
+            return self.executor.submit(callable_, *args, **kwargs)
+
+    expected_result = "expected_result"
+
+    def sync_func():
+        return expected_result
+
+    custom_executor = CustomExecutor()
+    async_function = sync_to_async(
+        sync_func, thread_sensitive=False, executor=custom_executor
+    )
+    actual_result = await async_function()
+    assert actual_result == expected_result
+    assert custom_executor.times_submit_called == 1
+
+    pytest.raises(
+        TypeError,
+        sync_to_async,
+        sync_func,
+        thread_sensitive=True,
+        executor=custom_executor,
+    )
+
+
+@pytest.mark.skipif(sys.version_info < (3, 7), reason="Issue persists with 3.6")
+def test_sync_to_async_deadlock_raises():
+    def db_write():
+        pass
+
+    async def io_task():
+        await sync_to_async(db_write)()
+
+    async def do_io_tasks():
+        t = asyncio.create_task(io_task())
+        await t
+        # await asyncio.gather(io_task()) # Also deadlocks
+        # await io_task() # Works
+
+    def view():
+        async_to_sync(do_io_tasks)()
+
+    async def server_entry():
+        await sync_to_async(view)()
+
+    with pytest.raises(RuntimeError):
+        asyncio.run(server_entry())
+
+
+@pytest.mark.skipif(sys.version_info < (3, 7), reason="Issue persists with 3.6")
+def test_sync_to_async_deadlock_ignored_with_exception():
+    """
+    Ensures that throwing an exception from inside a deadlock-protected block
+    still resets the deadlock detector's status.
+    """
+
+    def view():
+        raise ValueError()
+
+    async def server_entry():
+        try:
+            await sync_to_async(view)()
+        except ValueError:
+            pass
+        try:
+            await sync_to_async(view)()
+        except ValueError:
+            pass
+
+    asyncio.run(server_entry())
diff --git a/tests/test_sync_contextvars.py b/tests/test_sync_contextvars.py
index b1027aa..9665bf9 100644
--- a/tests/test_sync_contextvars.py
+++ b/tests/test_sync_contextvars.py
@@ -4,6 +4,7 @@ import time
 
 import pytest
 
+from asgiref.compatibility import create_task
 from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async
 
 contextvars = pytest.importorskip("contextvars")
@@ -25,7 +26,7 @@ async def test_thread_sensitive_with_context_different():
             await store_thread(result)
 
     # Run it (in true parallel!)
-    await asyncio.wait([fn(result_1), fn(result_2)])
+    await asyncio.wait([create_task(fn(result_1)), create_task(fn(result_2))])
 
     # They should not have run in the main thread, and on different threads
     assert result_1["thread"] != threading.current_thread()
diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py
index 820aacd..aabea4d 100644
--- a/tests/test_wsgi.py
+++ b/tests/test_wsgi.py
@@ -13,7 +13,7 @@ async def test_basic_wsgi():
     """
     # Define WSGI app
     def wsgi_application(environ, start_response):
-        assert environ["HTTP_TEST_HEADER"] == "test value"
+        assert environ["HTTP_TEST_HEADER"] == "test value 1,test value 2"
         start_response("200 OK", [["X-Colour", "Blue"]])
         yield b"first chunk "
         yield b"second chunk"
@@ -29,7 +29,10 @@ async def test_basic_wsgi():
             "method": "GET",
             "path": "/foo/",
             "query_string": b"bar=baz",
-            "headers": [[b"test-header", b"test value"]],
+            "headers": [
+                [b"test-header", b"test value 1"],
+                [b"test-header", b"test value 2"],
+            ],
         },
     )
     await instance.send_input({"type": "http.request"})