├── tests ├── __init__.py ├── sample_project │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_selenium.py │ ├── config │ │ ├── __init__.py │ │ ├── wsgi.py │ │ ├── urls.py │ │ ├── asgi.py │ │ └── settings.py │ ├── sampleapp │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── apps.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── templates │ │ │ └── admin │ │ │ │ └── sampleapp │ │ │ │ └── message │ │ │ │ └── change_list.html │ │ ├── static │ │ │ └── sampleapp │ │ │ │ ├── images │ │ │ │ └── django.svg │ │ │ │ ├── css │ │ │ │ └── styles.css │ │ │ │ └── js │ │ │ │ └── scripts.js │ │ └── consumers.py │ └── manage.py ├── conftest.py ├── test_database.py ├── security │ └── test_websocket.py ├── test_generic_http.py ├── test_layers.py └── test_testing.py ├── channels ├── generic │ ├── __init__.py │ └── http.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── runworker.py ├── security │ ├── __init__.py │ └── websocket.py ├── __init__.py ├── apps.py ├── testing │ ├── __init__.py │ ├── application.py │ ├── http.py │ ├── live.py │ └── websocket.py ├── db.py ├── middleware.py ├── exceptions.py ├── worker.py ├── utils.py ├── consumer.py └── routing.py ├── docs ├── requirements.txt ├── releases │ ├── 4.3.1.rst │ ├── 2.2.0.rst │ ├── 3.0.1.rst │ ├── 4.3.2.rst │ ├── 4.2.1.rst │ ├── 1.0.1.rst │ ├── 2.1.5.rst │ ├── 4.2.2.rst │ ├── 1.1.5.rst │ ├── 3.0.2.rst │ ├── 3.0.5.rst │ ├── 1.1.1.rst │ ├── 1.1.6.rst │ ├── 1.1.3.rst │ ├── index.rst │ ├── 2.1.6.rst │ ├── 2.4.0.rst │ ├── 2.1.3.rst │ ├── 1.1.2.rst │ ├── 4.3.0.rst │ ├── 1.0.3.rst │ ├── 3.0.4.rst │ ├── 2.0.2.rst │ ├── 4.1.0.rst │ ├── 2.1.7.rst │ ├── 1.1.4.rst │ ├── 1.0.2.rst │ ├── 2.3.0.rst │ ├── 2.1.4.rst │ ├── 2.1.1.rst │ ├── 1.1.0.rst │ ├── 2.0.1.rst │ ├── 2.0.0.rst │ ├── 2.1.2.rst │ ├── 3.0.3.rst │ ├── 4.2.0.rst │ ├── 3.0.0.rst │ ├── 2.1.0.rst │ └── 4.0.0.rst ├── tutorial │ ├── index.rst │ └── part_3.rst ├── topics │ ├── troubleshooting.rst │ ├── security.rst │ ├── sessions.rst │ ├── worker.rst │ ├── databases.rst │ ├── routing.rst │ └── authentication.rst ├── includes │ └── asgi_example.rst ├── index.rst ├── asgi.rst ├── community.rst ├── installation.rst ├── contributing.rst └── support.rst ├── MANIFEST.in ├── .github ├── FUNDING.yml ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── dependabot.yml ├── ISSUE_TEMPLATE.md └── workflows │ └── tests.yml ├── pyproject.toml ├── loadtesting ├── 2016-09-06 │ ├── channels-latency.PNG │ ├── channels-throughput.PNG │ └── README.rst └── README.md ├── .coveragerc ├── .gitignore ├── tox.ini ├── .readthedocs.yaml ├── CONTRIBUTING.rst ├── LICENSE ├── setup.cfg └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /channels/generic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /channels/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /channels/security/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sample_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sample_project/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /channels/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sample_project/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude tests * 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.djangoproject.com/fundraising/ 2 | github: [django] 3 | -------------------------------------------------------------------------------- /channels/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.3.2" 2 | 3 | 4 | DEFAULT_CHANNEL_LAYER = "default" 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Django Security Policies 2 | 3 | Please see https://www.djangoproject.com/security/. 4 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project follows [Django's Code of Conduct](https://www.djangoproject.com/conduct/). 2 | -------------------------------------------------------------------------------- /loadtesting/2016-09-06/channels-latency.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django/channels/HEAD/loadtesting/2016-09-06/channels-latency.PNG -------------------------------------------------------------------------------- /loadtesting/2016-09-06/channels-throughput.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django/channels/HEAD/loadtesting/2016-09-06/channels-throughput.PNG -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /channels/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChannelsConfig(AppConfig): 5 | name = "channels" 6 | verbose_name = "Channels" 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = channels 4 | omit = tests/* 5 | 6 | [report] 7 | show_missing = True 8 | skip_covered = True 9 | omit = tests/* 10 | 11 | [html] 12 | directory = coverage_html 13 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SampleappConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "tests.sample_project.sampleapp" 7 | -------------------------------------------------------------------------------- /docs/releases/4.3.1.rst: -------------------------------------------------------------------------------- 1 | 4.3.1 Release Notes 2 | =================== 3 | 4 | Channels 4.3.1 is a bugfix release in the 4.3 series. 5 | 6 | Bugfixes & Small Changes 7 | ------------------------ 8 | 9 | * Fixed testing live server setup when test DB name was not set. 10 | -------------------------------------------------------------------------------- /docs/releases/2.2.0.rst: -------------------------------------------------------------------------------- 1 | 2.2.0 Release Notes 2 | =================== 3 | 4 | Channels 2.2.0 updates the requirements for ASGI version 3, and the supporting 5 | Daphne v2.3 release. 6 | 7 | Backwards Incompatible Changes 8 | ------------------------------ 9 | 10 | None. 11 | -------------------------------------------------------------------------------- /docs/releases/3.0.1.rst: -------------------------------------------------------------------------------- 1 | 3.0.1 Release Notes 2 | =================== 3 | 4 | Channels 3.0.1 fixes a bug in Channels 3.0. 5 | 6 | Bugfixes 7 | -------- 8 | 9 | * Fixes a bug in Channels 3.0 where ``SessionMiddleware`` would not correctly 10 | isolate per-instance scopes. 11 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Message 4 | 5 | 6 | @admin.register(Message) 7 | class MessageAdmin(admin.ModelAdmin): 8 | list_display = ("title", "created") 9 | change_list_template = "admin/sampleapp/message/change_list.html" 10 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Message(models.Model): 5 | title = models.CharField(max_length=255) 6 | message = models.TextField() 7 | created = models.DateTimeField(auto_now_add=True) 8 | 9 | def __str__(self): 10 | return self.title 11 | -------------------------------------------------------------------------------- /loadtesting/README.md: -------------------------------------------------------------------------------- 1 | Django Channels Load Testing Results Index 2 | =============== 3 | 4 | [2016-09-06 Results](2016-09-06/README.rst) 5 | --------------- 6 | 7 | **Normal Django, WSGI** 8 | - Gunicorn (19.6.0) 9 | 10 | 11 | **Django Channels, ASGI** 12 | - Redis (0.14.0) and Daphne (0.14.3) 13 | - IPC (1.1.0) and Daphne (0.14.3) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | dist/ 3 | build/ 4 | docs/_build 5 | __pycache__/ 6 | .cache 7 | *.sqlite3 8 | .tox/ 9 | *.swp 10 | *.pyc 11 | .coverage* 12 | .pytest_cache 13 | TODO 14 | node_modules 15 | 16 | # Pipenv 17 | Pipfile 18 | Pipfile.lock 19 | 20 | # IDE and Tooling files 21 | .idea/* 22 | *~ 23 | .vscode 24 | 25 | # macOS 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /docs/releases/4.3.2.rst: -------------------------------------------------------------------------------- 1 | 4.3.2 Release Notes 2 | =================== 3 | 4 | Channels 4.3.1 is a bugfix release in the 4.3 series. 5 | 6 | Bugfixes & Small Changes 7 | ------------------------ 8 | 9 | * Confirmed support for Django 6.0. 10 | 11 | * Confirmed support for Python 3.14. 12 | 13 | * Added ``types`` extra for ``types-channels`` stubs. See installation docs. 14 | -------------------------------------------------------------------------------- /docs/releases/4.2.1.rst: -------------------------------------------------------------------------------- 1 | 4.2.1 Release Notes 2 | =================== 3 | 4 | Channels 4.2.1 is a bugfix release in the 4.2 series. 5 | 6 | Bugfixes & Small Changes 7 | ------------------------ 8 | 9 | * Added official support for Django 5.2 LTS. 10 | 11 | * Added official support for Python 3.13. 12 | 13 | * Added a warning for the length of the channel layer group names. 14 | -------------------------------------------------------------------------------- /channels/testing/__init__.py: -------------------------------------------------------------------------------- 1 | from .application import ApplicationCommunicator # noqa 2 | from .http import HttpCommunicator # noqa 3 | from .live import ChannelsLiveServerTestCase # noqa 4 | from .websocket import WebsocketCommunicator # noqa 5 | 6 | __all__ = [ 7 | "ApplicationCommunicator", 8 | "HttpCommunicator", 9 | "ChannelsLiveServerTestCase", 10 | "WebsocketCommunicator", 11 | ] 12 | -------------------------------------------------------------------------------- /docs/releases/1.0.1.rst: -------------------------------------------------------------------------------- 1 | 1.0.1 Release Notes 2 | =================== 3 | 4 | Channels 1.0.1 is a minor bugfix release, released on 2017/01/09. 5 | 6 | Changes 7 | ------- 8 | 9 | * WebSocket generic views now accept connections by default in their connect 10 | handler for better backwards compatibility. 11 | 12 | 13 | Backwards Incompatible Changes 14 | ------------------------------ 15 | 16 | None. 17 | -------------------------------------------------------------------------------- /docs/releases/2.1.5.rst: -------------------------------------------------------------------------------- 1 | 2.1.5 Release Notes 2 | =================== 3 | 4 | Channels 2.1.5 is another bugfix release in the 2.1 series. 5 | 6 | 7 | Bugfixes & Small Changes 8 | ------------------------ 9 | 10 | * Django middleware caching now works on Django 1.11 and Django 2.0. 11 | The previous release only ran on 2.1. 12 | 13 | 14 | Backwards Incompatible Changes 15 | ------------------------------ 16 | 17 | None. 18 | -------------------------------------------------------------------------------- /docs/releases/4.2.2.rst: -------------------------------------------------------------------------------- 1 | 4.2.2 Release Notes 2 | =================== 3 | 4 | Channels 4.2.2 is a bugfix release in the 4.2 series. 5 | 6 | Bugfixes & Small Changes 7 | ------------------------ 8 | 9 | * Added fallbacks for old valid channel/group name checks. 10 | 11 | These (internal) methods were renamed in v4.2.1 without deprecation. This 12 | release adds (deprecated) fallback aliases to allow time for channel layers 13 | to update. 14 | -------------------------------------------------------------------------------- /docs/releases/1.1.5.rst: -------------------------------------------------------------------------------- 1 | 1.1.5 Release Notes 2 | =================== 3 | 4 | Channels 1.1.5 is a packaging release for the 1.1 series, released on 5 | June 16th, 2017. 6 | 7 | 8 | Major Changes 9 | ------------- 10 | 11 | None. 12 | 13 | 14 | Minor Changes & Bugfixes 15 | ------------------------ 16 | 17 | * The Daphne dependency requirement was bumped to 1.3.0. 18 | 19 | Backwards Incompatible Changes 20 | ------------------------------ 21 | 22 | None. 23 | -------------------------------------------------------------------------------- /docs/releases/3.0.2.rst: -------------------------------------------------------------------------------- 1 | 3.0.2 Release Notes 2 | =================== 3 | 4 | Channels 3.0.2 fixes a bug in Channels 3.0.1 5 | 6 | Bugfixes 7 | -------- 8 | 9 | * Fixes a bug in Channels 3.0 where `StaticFilesWrapper` was not updated to 10 | the ASGI 3 single-callable interface. 11 | 12 | * Users of the ``runworker`` command should ensure to update ``asgiref`` to 13 | version 3.3.1 or later, where an issue in ``asgiref.server.StatelessServer`` 14 | was addressed. 15 | -------------------------------------------------------------------------------- /docs/releases/3.0.5.rst: -------------------------------------------------------------------------------- 1 | 3.0.5 Release Notes 2 | =================== 3 | 4 | Channels 3.0.5 is a bugfix release in the 3.0 series. 5 | 6 | Bugfixes & Small Changes 7 | ------------------------ 8 | 9 | * Removed use of ``providing_args`` keyword argument to consumer started 10 | signal, as support for this was removed in Django 4.0. 11 | 12 | Backwards Incompatible Changes 13 | ------------------------------ 14 | 15 | * Drops support for end-of-life Python 3.6 and Django 3.0 and 3.1. 16 | -------------------------------------------------------------------------------- /tests/sample_project/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sample_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docs/releases/1.1.1.rst: -------------------------------------------------------------------------------- 1 | 1.1.1 Release Notes 2 | =================== 3 | 4 | Channels 1.1.1 is a bugfix release that fixes a packaging issue with the JavaScript files. 5 | 6 | 7 | Major Changes 8 | ------------- 9 | 10 | None. 11 | 12 | Minor Changes & Bugfixes 13 | ------------------------ 14 | 15 | * The JavaScript binding introduced in 1.1.0 is now correctly packaged and 16 | included in builds. 17 | 18 | 19 | Backwards Incompatible Changes 20 | ------------------------------ 21 | 22 | None. 23 | -------------------------------------------------------------------------------- /docs/releases/1.1.6.rst: -------------------------------------------------------------------------------- 1 | 1.1.6 Release Notes 2 | =================== 3 | 4 | Channels 1.1.5 is a packaging release for the 1.1 series, released on 5 | June 28th, 2017. 6 | 7 | 8 | Major Changes 9 | ------------- 10 | 11 | None. 12 | 13 | 14 | Minor Changes & Bugfixes 15 | ------------------------ 16 | 17 | * The ``runserver`` ``server_cls`` override no longer fails with more modern 18 | Django versions that pass an ``ipv6`` parameter. 19 | 20 | Backwards Incompatible Changes 21 | ------------------------------ 22 | 23 | None. 24 | -------------------------------------------------------------------------------- /docs/releases/1.1.3.rst: -------------------------------------------------------------------------------- 1 | 1.1.3 Release Notes 2 | =================== 3 | 4 | Channels 1.1.3 is a bugfix release for the 1.1 series, released on 5 | April 5th, 2017. 6 | 7 | 8 | Major Changes 9 | ------------- 10 | 11 | None. 12 | 13 | 14 | Minor Changes & Bugfixes 15 | ------------------------ 16 | 17 | * ``enforce_ordering`` now works correctly with the new-style process-specific 18 | channels 19 | 20 | * ASGI channel layer versions are now explicitly checked for version compatibility 21 | 22 | 23 | Backwards Incompatible Changes 24 | ------------------------------ 25 | 26 | None. 27 | -------------------------------------------------------------------------------- /channels/testing/application.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from asgiref.testing import ApplicationCommunicator as BaseApplicationCommunicator 4 | 5 | 6 | def no_op(): 7 | pass 8 | 9 | 10 | class ApplicationCommunicator(BaseApplicationCommunicator): 11 | async def send_input(self, message): 12 | with mock.patch("channels.db.close_old_connections", no_op): 13 | return await super().send_input(message) 14 | 15 | async def receive_output(self, timeout=1): 16 | with mock.patch("channels.db.close_old_connections", no_op): 17 | return await super().receive_output(timeout) 18 | -------------------------------------------------------------------------------- /docs/releases/index.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | 4.3.2 8 | 4.3.1 9 | 4.3.0 10 | 4.2.2 11 | 4.2.1 12 | 4.2.0 13 | 4.1.0 14 | 4.0.0 15 | 3.0.5 16 | 3.0.4 17 | 3.0.3 18 | 3.0.2 19 | 3.0.1 20 | 3.0.0 21 | 2.4.0 22 | 2.3.0 23 | 2.2.0 24 | 2.1.7 25 | 2.1.6 26 | 2.1.5 27 | 2.1.4 28 | 2.1.3 29 | 2.1.2 30 | 2.1.1 31 | 2.1.0 32 | 2.0.2 33 | 2.0.1 34 | 2.0.0 35 | 1.1.6 36 | 1.1.5 37 | 1.1.4 38 | 1.1.3 39 | 1.1.2 40 | 1.1.1 41 | 1.1.0 42 | 1.0.3 43 | 1.0.2 44 | 1.0.1 45 | 1.0.0 46 | -------------------------------------------------------------------------------- /docs/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | Channels allows you to use WebSockets and other non-HTTP protocols in your 5 | Django site. For example you might want to use WebSockets to allow a page on 6 | your site to immediately receive updates from your Django server without using 7 | HTTP long-polling or other expensive techniques. 8 | 9 | In this tutorial we will build a simple chat server, where you can join an 10 | online room, post messages to the room, and have others in the same room see 11 | those messages immediately. 12 | 13 | .. toctree:: 14 | :maxdepth: 1 15 | 16 | part_1 17 | part_2 18 | part_3 19 | part_4 20 | -------------------------------------------------------------------------------- /docs/releases/2.1.6.rst: -------------------------------------------------------------------------------- 1 | 2.1.6 Release Notes 2 | =================== 3 | 4 | Channels 2.1.6 is another bugfix release in the 2.1 series. 5 | 6 | 7 | Bugfixes & Small Changes 8 | ------------------------ 9 | 10 | * HttpCommunicator now extracts query strings correctly from its provided 11 | arguments 12 | 13 | * AsyncHttpConsumer provides channel layer attributes following the same 14 | conventions as other consumer classes 15 | 16 | * Prevent late-Daphne import errors where importing ``daphne.server`` didn't 17 | work due to a bad linter fix. 18 | 19 | 20 | Backwards Incompatible Changes 21 | ------------------------------ 22 | 23 | None. 24 | -------------------------------------------------------------------------------- /docs/releases/2.4.0.rst: -------------------------------------------------------------------------------- 1 | 2.4.0 Release Notes 2 | =================== 3 | 4 | Channels 2.4 brings compatibility with Django 3.0s ``async_unsafe()`` checks. 5 | (Specifically we ensure session save calls are made inside an asgiref 6 | ``database_sync_to_async()``.) 7 | 8 | If you are using Daphne, it is recommended that you install Daphne version 9 | 2.4.1 or later for full compatibility with Django 3.0. 10 | 11 | Backwards Incompatible Changes 12 | ------------------------------ 13 | 14 | In line with the guidance provided by Django's supported versions policy we now 15 | also drop support for all Django versions before 2.2, which is the current LTS. 16 | 17 | -------------------------------------------------------------------------------- /docs/releases/2.1.3.rst: -------------------------------------------------------------------------------- 1 | 2.1.3 Release Notes 2 | =================== 3 | 4 | Channels 2.1.3 is another bugfix release in the 2.1 series. 5 | 6 | 7 | Bugfixes & Small Changes 8 | ------------------------ 9 | 10 | * An ALLOWED_ORIGINS value of "*" will now also allow requests without a Host 11 | header at all (especially important for tests) 12 | 13 | * The request.path value is now correct in cases when a server has SCRIPT_NAME 14 | set. 15 | 16 | * Errors that happen inside channel listeners inside a runworker or Worker 17 | class are now raised rather than suppressed. 18 | 19 | 20 | Backwards Incompatible Changes 21 | ------------------------------ 22 | 23 | None. 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{39,310,311}-dj42 4 | py{310,311,312,313}-dj51 5 | py{310,311,312,313,314}-dj52 6 | py{312,313,314}-dj60 7 | py{312,313,314}-djmain 8 | qa 9 | 10 | [testenv] 11 | extras = tests, daphne 12 | commands = 13 | pytest -v {posargs} 14 | deps = 15 | dj42: Django>=4.2,<5.0 16 | dj51: Django>=5.1,<5.2 17 | dj52: Django>=5.2,<6.0 18 | dj60: Django>=6.0rc1,<6.1 19 | djmain: https://github.com/django/django/archive/main.tar.gz 20 | 21 | [testenv:qa] 22 | skip_install=true 23 | deps = 24 | black 25 | flake8 26 | isort 27 | commands = 28 | flake8 channels tests 29 | black --check channels tests 30 | isort --check-only --diff channels tests 31 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from django.conf import settings 5 | 6 | 7 | def pytest_configure(): 8 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.sample_project.config.settings" 9 | settings._setup() 10 | 11 | 12 | def pytest_generate_tests(metafunc): 13 | if "samesite" in metafunc.fixturenames: 14 | metafunc.parametrize("samesite", ["Strict", "None"], indirect=True) 15 | 16 | 17 | @pytest.fixture 18 | def samesite(request, settings): 19 | """Set samesite flag to strict.""" 20 | settings.SESSION_COOKIE_SAMESITE = request.param 21 | 22 | 23 | @pytest.fixture 24 | def samesite_invalid(settings): 25 | """Set samesite flag to strict.""" 26 | settings.SESSION_COOKIE_SAMESITE = "Hello" 27 | -------------------------------------------------------------------------------- /channels/db.py: -------------------------------------------------------------------------------- 1 | from asgiref.sync import SyncToAsync, sync_to_async 2 | from django.db import close_old_connections 3 | 4 | 5 | class DatabaseSyncToAsync(SyncToAsync): 6 | """ 7 | SyncToAsync version that cleans up old database connections when it exits. 8 | """ 9 | 10 | def thread_handler(self, loop, *args, **kwargs): 11 | close_old_connections() 12 | try: 13 | return super().thread_handler(loop, *args, **kwargs) 14 | finally: 15 | close_old_connections() 16 | 17 | 18 | # The class is TitleCased, but we want to encourage use as a callable/decorator 19 | database_sync_to_async = DatabaseSyncToAsync 20 | 21 | 22 | async def aclose_old_connections(): 23 | return await sync_to_async(close_old_connections)() 24 | -------------------------------------------------------------------------------- /tests/sample_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /docs/releases/1.1.2.rst: -------------------------------------------------------------------------------- 1 | 1.1.2 Release Notes 2 | =================== 3 | 4 | Channels 1.1.2 is a bugfix release for the 1.1 series, released on 5 | April 1st, 2017. 6 | 7 | 8 | Major Changes 9 | ------------- 10 | 11 | None. 12 | 13 | 14 | Minor Changes & Bugfixes 15 | ------------------------ 16 | 17 | * Session name hash changed to SHA-1 to satisfy FIPS-140-2. 18 | 19 | * `scheme` key in ASGI-HTTP messages now translates into `request.is_secure()` 20 | correctly. 21 | 22 | * WebsocketBridge now exposes the underlying WebSocket as `.socket`. 23 | 24 | 25 | Backwards Incompatible Changes 26 | ------------------------------ 27 | 28 | * When you upgrade all current channel sessions will be invalidated; you 29 | should make sure you disconnect all WebSockets during upgrade. 30 | -------------------------------------------------------------------------------- /docs/releases/4.3.0.rst: -------------------------------------------------------------------------------- 1 | 4.3.0 Release Notes 2 | =================== 3 | 4 | Channels 4.3 is a maintenance release in the 4.x series. 5 | 6 | Bugfixes & Small Changes 7 | ------------------------ 8 | 9 | * Updated asgiref dependency to v3.9+. 10 | 11 | The ``ApplicationCommunicator`` testing utility will now return its result if 12 | the application is finished when sending input. Assert the 13 | ``CancelledError``` rather than allowing a timeout in your tests if you're 14 | affected by this change. 15 | 16 | * Dropped support for EOL Python and Django versions. Python 3.9 is now the 17 | minimum supported version. 18 | 19 | * Fixed compatibility of ``ChannelsLiveServerTestCase`` with Django 5.2. 20 | 21 | * Fixed DB setup for spawned testing subprocess, typically on Windows and macOS. 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issues are for **concrete, actionable bugs and feature requests** only - if you're just asking for debugging help or technical support we have to direct you elsewhere. If you just have questions or support requests please use: 2 | 3 | - Stack Overflow 4 | - The Django Users mailing list django-users@googlegroups.com (https://groups.google.com/forum/#!forum/django-users) 5 | 6 | We have to limit this because of limited volunteer time to respond to issues! 7 | 8 | Please also try and include, if you can: 9 | 10 | - Your OS and runtime environment, and browser if applicable 11 | - A `pip freeze` output showing your package versions 12 | - What you expected to happen vs. what actually happened 13 | - How you're running Channels (runserver? daphne/runworker? Nginx/Apache in front?) 14 | - Console logs and full tracebacks of any errors 15 | -------------------------------------------------------------------------------- /docs/releases/1.0.3.rst: -------------------------------------------------------------------------------- 1 | 1.0.3 Release Notes 2 | =================== 3 | 4 | Channels 1.0.3 is a minor bugfix release, released on 2017/02/01. 5 | 6 | Changes 7 | ------- 8 | 9 | * Database connections are no longer force-closed after each test is run. 10 | 11 | * Channel sessions are not re-saved if they're empty even if they're marked as 12 | modified, allowing logout to work correctly. 13 | 14 | * WebsocketDemultiplexer now correctly does sessions for the second/third/etc. 15 | connect and disconnect handlers. 16 | 17 | * Request reading timeouts now correctly return 408 rather than erroring out. 18 | 19 | * The ``rundelay`` delay server now only polls the database once per second, 20 | and this interval is configurable with the ``--sleep`` option. 21 | 22 | 23 | Backwards Incompatible Changes 24 | ------------------------------ 25 | 26 | None. 27 | -------------------------------------------------------------------------------- /channels/middleware.py: -------------------------------------------------------------------------------- 1 | class BaseMiddleware: 2 | """ 3 | Base class for implementing ASGI middleware. 4 | 5 | Note that subclasses of this are not self-safe; don't store state on 6 | the instance, as it serves multiple application instances. Instead, use 7 | scope. 8 | """ 9 | 10 | def __init__(self, inner): 11 | """ 12 | Middleware constructor - just takes inner application. 13 | """ 14 | self.inner = inner 15 | 16 | async def __call__(self, scope, receive, send): 17 | """ 18 | ASGI application; can insert things into the scope and run asynchronous 19 | code. 20 | """ 21 | # Copy scope to stop changes going upstream 22 | scope = dict(scope) 23 | # Run the inner application along with the scope 24 | return await self.inner(scope, receive, send) 25 | -------------------------------------------------------------------------------- /docs/releases/3.0.4.rst: -------------------------------------------------------------------------------- 1 | 3.0.4 Release Notes 2 | =================== 3 | 4 | Channels 3.0.4 is a bugfix release in the 3.0 series. 5 | 6 | 7 | Bugfixes & Small Changes 8 | ------------------------ 9 | 10 | * Usage of ``urlparse`` in ``OriginValidator`` is corrected to maintain 11 | compatibility with recent point-releases of Python. 12 | 13 | * The import of ``django.contrib.auth.models.AnonymousUser`` in 14 | ``channels.auth`` is deferred until runtime, in order to avoid errors if 15 | ``AuthMiddleware`` or ``AuthMiddlewareStack`` were imported before 16 | ``django.setup()`` was run. 17 | 18 | * ``CookieMiddleware`` adds support for the ``samesite`` flag. 19 | 20 | * ``WebsocketConsumer.init()`` and ``AsyncWebsocketConsumer.init()`` no longer 21 | make a bad `super()` call to ``object.init()``. 22 | 23 | 24 | Backwards Incompatible Changes 25 | ------------------------------ 26 | 27 | None. 28 | -------------------------------------------------------------------------------- /docs/releases/2.0.2.rst: -------------------------------------------------------------------------------- 1 | 2.0.2 Release Notes 2 | =================== 3 | 4 | Channels 2.0.2 is a patch release of Channels, fixing a bug in the database 5 | connection handling. 6 | 7 | As always, when updating Channels make sure to also update its dependencies 8 | (``asgiref`` and ``daphne``) as these also get their own bugfix updates, and 9 | some bugs that may appear to be part of Channels are actually in those packages. 10 | 11 | 12 | New Features 13 | ------------ 14 | 15 | * There is a new ``channels.db.database_sync_to_async`` wrapper that is like 16 | ``sync_to_async`` but also closes database connections for you. You can 17 | read more about usage in :doc:`/topics/databases`. 18 | 19 | 20 | Bugfixes 21 | -------- 22 | 23 | * SyncConsumer and all its descendant classes now close database connections 24 | when they exit. 25 | 26 | 27 | Backwards Incompatible Changes 28 | ------------------------------ 29 | 30 | None. 31 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2 on 2025-05-25 11:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Message", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("title", models.CharField(max_length=255)), 26 | ("message", models.TextField()), 27 | ("created", models.DateTimeField(auto_now_add=True)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /docs/releases/4.1.0.rst: -------------------------------------------------------------------------------- 1 | 4.1.0 Release Notes 2 | =================== 3 | 4 | Channels 4.1 is maintenance release in the 4.x series. 5 | 6 | 7 | Python and Django support 8 | ------------------------- 9 | 10 | * A Python version of 3.8 or higher is required. 11 | 12 | * Django 4.2 is now the minimum supported version. 13 | 14 | 15 | Bugfixes & Small Changes 16 | ------------------------ 17 | 18 | * Exceptions in ``HttpConsumer`` are now correctly propagated. 19 | 20 | Thanks to Adam Johnson. 21 | 22 | * URLRouter is updated for compatibility with in-development changes in Django. 23 | 24 | Thanks to Adam Johnson. 25 | 26 | * URLRouter is updated to correctly handle ``root_path``. 27 | 28 | Thanks to Alejandro R. Sedeño. 29 | 30 | * Websocket consumers are updated for newer ASGI spec versions, adding the 31 | ``headers`` parameter for the ``accept`` event, and ``reason`` for the 32 | ``close`` event. 33 | 34 | Thanks to Kristján Valur Jónsson. 35 | -------------------------------------------------------------------------------- /docs/releases/2.1.7.rst: -------------------------------------------------------------------------------- 1 | 2.1.7 Release Notes 2 | =================== 3 | 4 | Channels 2.1.7 is another bugfix release in the 2.1 series, and the last 5 | release (at least for a long while) with Andrew Godwin as the primary 6 | maintainer. 7 | 8 | Thanks to everyone who has used, supported, and contributed to Channels over 9 | the years, and I hope we can keep it going with community support for a good 10 | while longer. 11 | 12 | 13 | Bugfixes & Small Changes 14 | ------------------------ 15 | 16 | * HTTP request body size limit is now enforced (the one set by the 17 | ``DATA_UPLOAD_MAX_MEMORY_SIZE`` setting) 18 | 19 | * ``database_sync_to_async`` now closes old connections before it runs code, 20 | which should prevent some connection errors in long-running pages or tests. 21 | 22 | * The auth middleware closes old connections before it runs, to solve similar 23 | old-connection issues. 24 | 25 | 26 | Backwards Incompatible Changes 27 | ------------------------------ 28 | 29 | None. 30 | -------------------------------------------------------------------------------- /docs/releases/1.1.4.rst: -------------------------------------------------------------------------------- 1 | 1.1.4 Release Notes 2 | =================== 3 | 4 | Channels 1.1.4 is a bugfix release for the 1.1 series, released on 5 | June 15th, 2017. 6 | 7 | 8 | Major Changes 9 | ------------- 10 | 11 | None. 12 | 13 | 14 | Minor Changes & Bugfixes 15 | ------------------------ 16 | 17 | * Pending messages correctly handle retries in backlog situations 18 | 19 | * Workers in threading mode now respond to ctrl-C and gracefully exit. 20 | 21 | * ``request.meta['QUERY_STRING']`` is now correctly encoded at all times. 22 | 23 | * Test client improvements 24 | 25 | * ``ChannelServerLiveTestCase`` added, allows an equivalent of the Django 26 | ``LiveTestCase``. 27 | 28 | * Decorator added to check ``Origin`` headers (``allowed_hosts_only``) 29 | 30 | * New ``TEST_CONFIG`` setting in ``CHANNEL_LAYERS`` that allows varying of 31 | the channel layer for tests (e.g. using a different Redis install) 32 | 33 | 34 | Backwards Incompatible Changes 35 | ------------------------------ 36 | 37 | None. 38 | -------------------------------------------------------------------------------- /docs/releases/1.0.2.rst: -------------------------------------------------------------------------------- 1 | 1.0.2 Release Notes 2 | =================== 3 | 4 | Channels 1.0.2 is a minor bugfix release, released on 2017/01/12. 5 | 6 | Changes 7 | ------- 8 | 9 | * Websockets can now be closed from anywhere using the new ``WebsocketCloseException``, 10 | available as ``channels.exceptions.WebsocketCloseException(code=None)``. There is 11 | also a generic ``ChannelSocketException`` you can base any exceptions on that, 12 | if it is caught, gets handed the current ``message`` in a ``run`` method, so you 13 | can do custom behaviours. 14 | 15 | * Calling ``Channel.send`` or ``Group.send`` from outside a consumer context 16 | (i.e. in tests or management commands) will once again send the message immediately, 17 | rather than putting it into the consumer message buffer to be flushed when the 18 | consumer ends (which never happens) 19 | 20 | * The base implementation of databinding now correctly only calls ``group_names(instance)``, 21 | as documented. 22 | 23 | 24 | Backwards Incompatible Changes 25 | ------------------------------ 26 | 27 | None. 28 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/templates/admin/sampleapp/message/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load static %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {{ block.super }} 11 | 12 |
13 |

Live Messages

14 |
15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 |
23 | Total messages: 0 24 |
25 | 26 |
27 |
28 | {% endblock %} 29 | 30 | {% block footer %} 31 | 32 | {{ block.super }} 33 | {% endblock %} -------------------------------------------------------------------------------- /docs/releases/2.3.0.rst: -------------------------------------------------------------------------------- 1 | 2.3.0 Release Notes 2 | =================== 3 | 4 | Channels 2.3.0 updates the ``AsgiHandler`` HTTP request body handling to use a 5 | spooled temporary file, rather than reading the whole request body into memory. 6 | 7 | This significantly reduces the maximum memory requirements when serving Django 8 | views, and protects from DoS attacks, whilst still allowing large file 9 | uploads — a combination that had previously been *difficult*. 10 | 11 | Many thanks to Ivan Ergunov for his work on the improvements! 🎩 12 | 13 | Backwards Incompatible Changes 14 | ------------------------------ 15 | 16 | As a result of the reworked body handling, ``AsgiRequest.__init__()`` is 17 | adjusted to expect a file-like ``stream``, rather than the whole ``body`` as 18 | bytes. 19 | 20 | Test cases instantiating requests directly will likely need to be updated to 21 | wrap the provided ``body`` in, e.g., ``io.BytesIO``. 22 | 23 | Next Up... 24 | ---------- 25 | 26 | We're looking to address a few issues around ``AsyncHttpConsumer``. Any 27 | human-power available to help on that, truly appreciated. 🙂 28 | -------------------------------------------------------------------------------- /tests/sample_project/config/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for sample_project project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | 18 | from django.conf import settings 19 | from django.contrib import admin 20 | from django.urls import path 21 | from django.views.generic import RedirectView 22 | 23 | urlpatterns = [ 24 | path("admin/", admin.site.urls), 25 | path( 26 | "favicon.ico", 27 | RedirectView.as_view( 28 | url=settings.STATIC_URL + "sampleapp/images/django.svg", permanent=True 29 | ), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /docs/topics/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | 5 | 6 | ImproperlyConfigured exception 7 | ------------------------------ 8 | 9 | 10 | .. code-block:: text 11 | 12 | django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. 13 | You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings. 14 | 15 | 16 | This exception occurs when your application tries to import any models before Django finishes 17 | `its initialization process `_ aka ``django.setup()``. 18 | 19 | 20 | ``django.setup()`` `should be called only once `_, 21 | and should be called manually only in case of standalone apps. 22 | In context of Channels usage, ``django.setup()`` is called automatically in ``get_asgi_application()``, 23 | which means it needs to be called before any ORM models are imported. 24 | 25 | The working code order would look like this: 26 | 27 | .. include:: ../includes/asgi_example.rst 28 | -------------------------------------------------------------------------------- /docs/releases/2.1.4.rst: -------------------------------------------------------------------------------- 1 | 2.1.4 Release Notes 2 | =================== 3 | 4 | Channels 2.1.4 is another bugfix release in the 2.1 series. 5 | 6 | 7 | Bugfixes & Small Changes 8 | ------------------------ 9 | 10 | * Django middleware is now cached rather than instantiated per request 11 | resulting in a significant speed improvement. Some middleware took seconds to 12 | load and as a result Channels was unusable for HTTP serving before. 13 | 14 | * ChannelServerLiveTestCase now serves static files again. 15 | 16 | * Improved error message resulting from bad Origin headers. 17 | 18 | * ``runserver`` logging now goes through the Django logging framework to match 19 | modern Django. 20 | 21 | * Generic consumers can now have non-default channel layers - set the 22 | ``channel_layer_alias`` property on the consumer class 23 | 24 | * Improved error when accessing ``scope['user']`` before it's ready - the user 25 | is not accessible in the constructor of ASGI apps as it needs an async 26 | environment to load in. Previously it raised a generic error when you tried to 27 | access it early; now it tells you more clearly what's happening. 28 | 29 | 30 | Backwards Incompatible Changes 31 | ------------------------------ 32 | 33 | None. 34 | -------------------------------------------------------------------------------- /docs/includes/asgi_example.rst: -------------------------------------------------------------------------------- 1 | .. code-block:: python 2 | 3 | import os 4 | 5 | from channels.auth import AuthMiddlewareStack 6 | from channels.routing import ProtocolTypeRouter, URLRouter 7 | from channels.security.websocket import AllowedHostsOriginValidator 8 | from django.core.asgi import get_asgi_application 9 | from django.urls import path 10 | 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 12 | # Initialize Django ASGI application early to ensure the AppRegistry 13 | # is populated before importing code that may import ORM models. 14 | django_asgi_app = get_asgi_application() 15 | 16 | from chat.consumers import AdminChatConsumer, PublicChatConsumer 17 | 18 | application = ProtocolTypeRouter({ 19 | # Django's ASGI application to handle traditional HTTP requests 20 | "http": django_asgi_app, 21 | 22 | # WebSocket chat handler 23 | "websocket": AllowedHostsOriginValidator( 24 | AuthMiddlewareStack( 25 | URLRouter([ 26 | path("chat/admin/", AdminChatConsumer.as_asgi()), 27 | path("chat/", PublicChatConsumer.as_asgi()), 28 | ]) 29 | ) 30 | ), 31 | }) -------------------------------------------------------------------------------- /tests/sample_project/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for sample_project project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ 8 | """ 9 | 10 | from django.core.asgi import get_asgi_application 11 | from django.urls import path 12 | 13 | application = get_asgi_application() 14 | 15 | from channels.auth import AuthMiddlewareStack 16 | from channels.routing import ProtocolTypeRouter, URLRouter 17 | from channels.security.websocket import AllowedHostsOriginValidator 18 | from tests.sample_project.sampleapp.consumers import LiveMessageConsumer 19 | 20 | application = ProtocolTypeRouter( 21 | { 22 | "websocket": AllowedHostsOriginValidator( 23 | AuthMiddlewareStack( 24 | URLRouter( 25 | [ 26 | path( 27 | "ws/message/", 28 | LiveMessageConsumer.as_asgi(), 29 | name="live_message_counter", 30 | ), 31 | ] 32 | ) 33 | ) 34 | ), 35 | "http": application, 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt 20 | 21 | # Build documentation in the "docs/" directory with Sphinx 22 | sphinx: 23 | configuration: docs/conf.py 24 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 25 | # builder: "dirhtml" 26 | # Fail on all warnings to avoid broken references 27 | # fail_on_warning: true 28 | 29 | # Optionally build your docs in additional formats such as PDF and ePub 30 | # formats: 31 | # - pdf 32 | # - epub 33 | 34 | # Optional but recommended, declare the Python requirements required 35 | # to build your documentation 36 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 37 | # python: 38 | # install: 39 | # - requirements: docs/requirements.txt 40 | -------------------------------------------------------------------------------- /docs/releases/2.1.1.rst: -------------------------------------------------------------------------------- 1 | 2.1.1 Release Notes 2 | =================== 3 | 4 | Channels 2.1.1 is a bugfix release for an important bug in the new async 5 | authentication code. 6 | 7 | 8 | Major Changes 9 | ------------- 10 | 11 | None. 12 | 13 | 14 | Bugfixes & Small Changes 15 | ------------------------ 16 | 17 | Previously, the object in ``scope["user"]`` was one of Django's 18 | SimpleLazyObjects, which then called our ``get_user`` async function via 19 | ``async_to_sync``. 20 | 21 | This worked fine when called from SyncConsumers, but because 22 | async environments do not run attribute access in an async fashion, when 23 | the body of an async consumer tried to call it, the ``asgiref`` library 24 | flagged an error where the code was trying to call a synchronous function 25 | during a async context. 26 | 27 | To fix this, the User object is now loaded non-lazily on application startup. 28 | This introduces a blocking call during the synchronous application 29 | constructor, so the ASGI spec has been updated to recommend that constructors 30 | for ASGI apps are called in a threadpool and Daphne 2.1.1 implements this 31 | and is recommended for use with this release. 32 | 33 | 34 | Backwards Incompatible Changes 35 | ------------------------------ 36 | 37 | None. 38 | -------------------------------------------------------------------------------- /docs/releases/1.1.0.rst: -------------------------------------------------------------------------------- 1 | 1.1.0 Release Notes 2 | =================== 3 | 4 | Channels 1.1.0 introduces a couple of major but backwards-compatible changes, 5 | including most notably the inclusion of a standard, framework-agnostic JavaScript 6 | library for easier integration with your site. 7 | 8 | 9 | Major Changes 10 | ------------- 11 | 12 | * Channels now includes a JavaScript wrapper that wraps reconnection and 13 | multiplexing for you on the client side. For more on how to use it, see the 14 | javascript documentation. 15 | 16 | * Test classes have been moved from ``channels.tests`` to ``channels.test`` 17 | to better match Django. Old imports from ``channels.tests`` will continue to 18 | work but will trigger a deprecation warning, and ``channels.tests`` will be 19 | removed completely in version 1.3. 20 | 21 | Minor Changes & Bugfixes 22 | ------------------------ 23 | 24 | * Bindings now support non-integer fields for primary keys on models. 25 | 26 | * The ``enforce_ordering`` decorator no longer suffers a race condition where 27 | it would drop messages under high load. 28 | 29 | * ``runserver`` no longer errors if the ``staticfiles`` app is not enabled in Django. 30 | 31 | 32 | Backwards Incompatible Changes 33 | ------------------------------ 34 | 35 | None. 36 | -------------------------------------------------------------------------------- /docs/releases/2.0.1.rst: -------------------------------------------------------------------------------- 1 | 2.0.1 Release Notes 2 | =================== 3 | 4 | Channels 2.0.1 is a patch release of channels, adding a couple of small 5 | new features and fixing one bug in URL resolution. 6 | 7 | As always, when updating Channels make sure to also update its dependencies 8 | (``asgiref`` and ``daphne``) as these also get their own bugfix updates, and 9 | some bugs that may appear to be part of Channels are actually in those packages. 10 | 11 | 12 | New Features 13 | ------------ 14 | 15 | * There are new async versions of the Websocket generic consumers, 16 | ``AsyncWebsocketConsumer`` and ``AsyncJsonWebsocketConsumer``. Read more 17 | about them in :doc:`/topics/consumers`. 18 | 19 | * The old ``allowed_hosts_only`` decorator has been removed (it was 20 | accidentally included in the 2.0 release but didn't work) and replaced with 21 | a new ``OriginValidator`` and ``AllowedHostsOriginValidator`` set of 22 | ASGI middleware. Read more in :doc:`/topics/security`. 23 | 24 | 25 | Bugfixes 26 | -------- 27 | 28 | * A bug in ``URLRouter`` which didn't allow you to match beyond the first 29 | URL in some situations has been resolved, and a test suite was added for 30 | URL resolution to prevent it happening again. 31 | 32 | 33 | Backwards Incompatible Changes 34 | ------------------------------ 35 | 36 | None. 37 | -------------------------------------------------------------------------------- /channels/exceptions.py: -------------------------------------------------------------------------------- 1 | class RequestAborted(Exception): 2 | """ 3 | Raised when the incoming request tells us it's aborted partway through 4 | reading the body. 5 | """ 6 | 7 | pass 8 | 9 | 10 | class RequestTimeout(RequestAborted): 11 | """ 12 | Aborted specifically due to timeout. 13 | """ 14 | 15 | pass 16 | 17 | 18 | class InvalidChannelLayerError(ValueError): 19 | """ 20 | Raised when a channel layer is configured incorrectly. 21 | """ 22 | 23 | pass 24 | 25 | 26 | class AcceptConnection(Exception): 27 | """ 28 | Raised during a websocket.connect (or other supported connection) handler 29 | to accept the connection. 30 | """ 31 | 32 | pass 33 | 34 | 35 | class DenyConnection(Exception): 36 | """ 37 | Raised during a websocket.connect (or other supported connection) handler 38 | to deny the connection. 39 | """ 40 | 41 | pass 42 | 43 | 44 | class ChannelFull(Exception): 45 | """ 46 | Raised when a channel cannot be sent to as it is over capacity. 47 | """ 48 | 49 | pass 50 | 51 | 52 | class MessageTooLarge(Exception): 53 | """ 54 | Raised when a message cannot be sent as it's too big. 55 | """ 56 | 57 | pass 58 | 59 | 60 | class StopConsumer(Exception): 61 | """ 62 | Raised when a consumer wants to stop and close down its application instance. 63 | """ 64 | 65 | pass 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to Channels 2 | ======================== 3 | 4 | As an open source project, Channels welcomes contributions of many forms. By participating in this project, you 5 | agree to abide by the Django `code of conduct `_. 6 | 7 | Examples of contributions include: 8 | 9 | * Code patches 10 | * Documentation improvements 11 | * Bug reports and patch reviews 12 | 13 | For more information, please see our `contribution guide `_. 14 | 15 | Quick Setup 16 | ----------- 17 | 18 | Fork, then clone the repo: 19 | 20 | .. code-block:: sh 21 | 22 | git clone git@github.com:your-username/channels.git 23 | 24 | Make sure the tests pass: 25 | 26 | .. code-block:: sh 27 | 28 | python -m pip install -e .[tests,daphne] 29 | pytest 30 | 31 | .. note:: 32 | If you're using ``zsh`` for your shell, the above command will fail with a 33 | ``zsh: no matches found: .[tests]`` error. 34 | To fix this use ``noglob``:: 35 | 36 | noglob python -m pip install -e .[tests] 37 | 38 | Make your change. Add tests for your change. Make the tests pass: 39 | 40 | .. code-block:: sh 41 | 42 | tox 43 | 44 | Make sure your code conforms to the coding style: 45 | 46 | .. code-block:: sh 47 | 48 | black ./channels ./tests 49 | isort --check-only --diff --recursive ./channels ./tests 50 | 51 | Push to your fork and `submit a pull request `_. 52 | -------------------------------------------------------------------------------- /docs/releases/2.0.0.rst: -------------------------------------------------------------------------------- 1 | 2.0.0 Release Notes 2 | =================== 3 | 4 | Channels 2.0 is a major rewrite of Channels, introducing a large amount of 5 | changes to the fundamental design and architecture of Channels. Notably: 6 | 7 | * Data is no longer transported over a channel layer between protocol server 8 | and application; instead, applications run inside their protocol servers 9 | (like with WSGI). 10 | 11 | * To achieve this, the entire core of channels is now built around Python's 12 | ``asyncio`` framework and runs async-native down until it hits either a 13 | Django view or a synchronous consumer. 14 | 15 | * Python 2.7 and 3.4 are no longer supported. 16 | 17 | More detailed information on the changes and tips on how to port your 18 | applications can be found in our ``/one-to-two`` documentation in the 2.x 19 | docs version. 20 | 21 | 22 | Backwards Incompatible Changes 23 | ------------------------------ 24 | 25 | Channels 2 is regrettably not backwards-compatible at all with Channels 1 26 | applications due to the large amount of re-architecting done to the code and 27 | the switch from synchronous to asynchronous runtimes. 28 | 29 | A migration guide is available in the 2.x docs version, and a lot of the basic 30 | concepts are the same, but the basic class structure and imports have changed. 31 | 32 | Our apologies for having to make a breaking change like this, but it was the 33 | only way to fix some of the fundamental design issues in Channels 1. Channels 1 34 | will continue to receive security and data-loss fixes for the foreseeable 35 | future, but no new features will be added. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Django Software Foundation and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: Python ${{ matrix.python-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: 17 | - "3.9" 18 | - "3.10" 19 | - "3.11" 20 | - "3.12" 21 | - "3.13" 22 | - "3.14" 23 | 24 | steps: 25 | - uses: actions/checkout@v6 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v6 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | allow-prereleases: true 32 | 33 | - name: Set up Chrome 34 | uses: browser-actions/setup-chrome@v2 35 | 36 | - name: Set up ChromeDriver 37 | uses: nanasess/setup-chromedriver@v2 38 | 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip wheel setuptools 42 | python -m pip install --upgrade tox 43 | 44 | - name: Run tox targets for ${{ matrix.python-version }} 45 | run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) 46 | 47 | lint: 48 | name: Lint 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v6 52 | 53 | - name: Set up Python 54 | uses: actions/setup-python@v6 55 | with: 56 | python-version: "3.11" 57 | 58 | - name: Install dependencies 59 | run: | 60 | python -m pip install --upgrade pip tox 61 | 62 | - name: Run lint 63 | run: tox -e qa 64 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/static/sampleapp/images/django.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /channels/management/commands/runworker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.management import BaseCommand, CommandError 4 | 5 | from channels import DEFAULT_CHANNEL_LAYER 6 | from channels.layers import get_channel_layer 7 | from channels.routing import get_default_application 8 | from channels.worker import Worker 9 | 10 | logger = logging.getLogger("django.channels.worker") 11 | 12 | 13 | class Command(BaseCommand): 14 | leave_locale_alone = True 15 | worker_class = Worker 16 | 17 | def add_arguments(self, parser): 18 | super(Command, self).add_arguments(parser) 19 | parser.add_argument( 20 | "--layer", 21 | action="store", 22 | dest="layer", 23 | default=DEFAULT_CHANNEL_LAYER, 24 | help="Channel layer alias to use, if not the default.", 25 | ) 26 | parser.add_argument("channels", nargs="+", help="Channels to listen on.") 27 | 28 | def handle(self, *args, **options): 29 | # Get the backend to use 30 | self.verbosity = options.get("verbosity", 1) 31 | # Get the channel layer they asked for (or see if one isn't configured) 32 | if "layer" in options: 33 | self.channel_layer = get_channel_layer(options["layer"]) 34 | else: 35 | self.channel_layer = get_channel_layer() 36 | if self.channel_layer is None: 37 | raise CommandError("You do not have any CHANNEL_LAYERS configured.") 38 | # Run the worker 39 | logger.info("Running worker for channels %s", options["channels"]) 40 | worker = self.worker_class( 41 | application=get_default_application(), 42 | channels=options["channels"], 43 | channel_layer=self.channel_layer, 44 | ) 45 | worker.run() 46 | -------------------------------------------------------------------------------- /docs/releases/2.1.2.rst: -------------------------------------------------------------------------------- 1 | 2.1.2 Release Notes 2 | =================== 3 | 4 | Channels 2.1.2 is another bugfix release in the 2.1 series. 5 | 6 | Special thanks to people at the DjangoCon Europe sprints who helped out with 7 | several of these fixes. 8 | 9 | 10 | Major Changes 11 | ------------- 12 | 13 | Session and authentication middleware has been overhauled to be non-blocking. 14 | Previously, these middlewares potentially did database or session store access 15 | in the synchronous ASGI constructor, meaning they would block the entire event 16 | loop while doing so. 17 | 18 | Instead, they have now been modified to add LazyObjects into the scope in the 19 | places where the session or user will be, and then when the processing goes 20 | through their asynchronous portion, those stores are accessed in a non-blocking 21 | fashion. 22 | 23 | This should be an un-noticeable change for end users, but if you see weird 24 | behaviour or an unresolved LazyObject, let us know. 25 | 26 | 27 | Bugfixes & Small Changes 28 | ------------------------ 29 | 30 | * AsyncHttpConsumer now has a disconnect() method you can override if you 31 | want to perform actions (such as leaving groups) when a long-running HTTP 32 | request disconnects. 33 | 34 | * URL routing context now includes default arguments from the URLconf in the 35 | context's ``url_route`` key, alongside captured arguments/groups from the 36 | URL pattern. 37 | 38 | * The FORCE_SCRIPT_NAME setting is now respected in ASGI mode, and lets you 39 | override where Django thinks the root URL of your application is mounted. 40 | 41 | * ALLOWED_HOSTS is now set correctly during LiveServerTests, meaning you will 42 | no longer get ``400 Bad Request`` errors during these test runs. 43 | 44 | 45 | Backwards Incompatible Changes 46 | ------------------------------ 47 | 48 | None. 49 | -------------------------------------------------------------------------------- /channels/worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asgiref.server import StatelessServer 4 | 5 | 6 | class Worker(StatelessServer): 7 | """ 8 | ASGI protocol server that surfaces events sent to specific channels 9 | on the channel layer into a single application instance. 10 | """ 11 | 12 | def __init__(self, application, channels, channel_layer, max_applications=1000): 13 | super().__init__(application, max_applications) 14 | self.channels = channels 15 | self.channel_layer = channel_layer 16 | if self.channel_layer is None: 17 | raise ValueError("Channel layer is not valid") 18 | 19 | async def handle(self): 20 | """ 21 | Listens on all the provided channels and handles the messages. 22 | """ 23 | # For each channel, launch its own listening coroutine 24 | listeners = [] 25 | for channel in self.channels: 26 | listeners.append(asyncio.ensure_future(self.listener(channel))) 27 | # Wait for them all to exit 28 | await asyncio.wait(listeners) 29 | # See if any of the listeners had an error (e.g. channel layer error) 30 | [listener.result() for listener in listeners] 31 | 32 | async def listener(self, channel): 33 | """ 34 | Single-channel listener 35 | """ 36 | while True: 37 | message = await self.channel_layer.receive(channel) 38 | if not message.get("type", None): 39 | raise ValueError("Worker received message with no type.") 40 | # Make a scope and get an application instance for it 41 | scope = {"type": "channel", "channel": channel} 42 | instance_queue = self.get_or_create_application_instance(channel, scope) 43 | # Run the message into the app 44 | await instance_queue.put(message) 45 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = channels 3 | version = attr: channels.__version__ 4 | url = http://github.com/django/channels 5 | author = Django Software Foundation 6 | author_email = foundation@djangoproject.com 7 | description = Brings async, event-driven capabilities to Django. 8 | long_description = file: README.rst 9 | long_description_content_type = text/x-rst 10 | license = BSD 11 | classifiers = 12 | Development Status :: 5 - Production/Stable 13 | Environment :: Web Environment 14 | Intended Audience :: Developers 15 | License :: OSI Approved :: BSD License 16 | Operating System :: OS Independent 17 | Programming Language :: Python 18 | Programming Language :: Python :: 3 19 | Programming Language :: Python :: 3.9 20 | Programming Language :: Python :: 3.10 21 | Programming Language :: Python :: 3.11 22 | Programming Language :: Python :: 3.12 23 | Programming Language :: Python :: 3.13 24 | Programming Language :: Python :: 3.14 25 | Framework :: Django 26 | Framework :: Django :: 4.2 27 | Framework :: Django :: 5.1 28 | Framework :: Django :: 5.2 29 | Framework :: Django :: 6.0 30 | Topic :: Internet :: WWW/HTTP 31 | 32 | [options] 33 | packages = find: 34 | include_package_data = True 35 | install_requires = 36 | Django>=4.2 37 | asgiref>=3.9.0,<4 38 | python_requires = >=3.9 39 | 40 | [options.extras_require] 41 | tests = 42 | async-timeout 43 | coverage~=4.5 44 | pytest 45 | pytest-django 46 | pytest-asyncio 47 | selenium 48 | daphne = 49 | daphne>=4.0.0 50 | types = 51 | types-channels 52 | 53 | [options.packages.find] 54 | exclude = 55 | tests 56 | 57 | [flake8] 58 | exclude = venv/*,tox/*,docs/*,testproject/*,build/* 59 | max-line-length = 88 60 | extend-ignore = E203, W503 61 | per-file-ignores = 62 | tests/sample_project/config/asgi.py:E402 63 | 64 | [isort] 65 | profile = black 66 | 67 | [tool:pytest] 68 | testpaths = tests 69 | asyncio_mode = auto 70 | -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | from django import db 2 | from django.test import TestCase 3 | 4 | from channels.db import database_sync_to_async 5 | from channels.generic.http import AsyncHttpConsumer 6 | from channels.generic.websocket import AsyncWebsocketConsumer 7 | from channels.testing import HttpCommunicator, WebsocketCommunicator 8 | 9 | 10 | @database_sync_to_async 11 | def basic_query(): 12 | with db.connections["default"].cursor() as cursor: 13 | cursor.execute("SELECT 1234") 14 | return cursor.fetchone()[0] 15 | 16 | 17 | class WebsocketConsumer(AsyncWebsocketConsumer): 18 | async def connect(self): 19 | await basic_query() 20 | await self.accept("fun") 21 | 22 | 23 | class HttpConsumer(AsyncHttpConsumer): 24 | async def handle(self, body): 25 | await basic_query() 26 | await self.send_response( 27 | 200, 28 | b"", 29 | headers={b"Content-Type": b"text/plain"}, 30 | ) 31 | 32 | 33 | class ConnectionClosingTests(TestCase): 34 | async def test_websocket(self): 35 | self.assertNotRegex( 36 | db.connections["default"].settings_dict.get("NAME"), 37 | "memorydb", 38 | "This bug only occurs when the database is materialized on disk", 39 | ) 40 | communicator = WebsocketCommunicator(WebsocketConsumer.as_asgi(), "/") 41 | connected, subprotocol = await communicator.connect() 42 | self.assertTrue(connected) 43 | self.assertEqual(subprotocol, "fun") 44 | 45 | async def test_http(self): 46 | self.assertNotRegex( 47 | db.connections["default"].settings_dict.get("NAME"), 48 | "memorydb", 49 | "This bug only occurs when the database is materialized on disk", 50 | ) 51 | communicator = HttpCommunicator( 52 | HttpConsumer.as_asgi(), method="GET", path="/test/" 53 | ) 54 | connected = await communicator.get_response() 55 | self.assertTrue(connected) 56 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django Channels 2 | =============== 3 | 4 | Channels is a project that takes Django and extends its abilities beyond 5 | HTTP - to handle WebSockets, chat protocols, IoT protocols, and more. It's 6 | built on a Python specification called `ASGI `_. 7 | 8 | Channels builds upon the native ASGI support in Django. Whilst Django still handles 9 | traditional HTTP, Channels gives you the choice to handle other connections in 10 | either a synchronous or asynchronous style. 11 | 12 | To get started understanding Channels, read our :doc:`introduction`, 13 | which will walk through how things work. 14 | 15 | .. note:: 16 | This is documentation for the **4.x series** of Channels. If you are looking 17 | for documentation for older versions, you can select ``3.x``, ``2.x``, or 18 | ``1.x`` from the versions selector in the bottom-left corner. 19 | 20 | Projects 21 | -------- 22 | 23 | Channels is comprised of several packages: 24 | 25 | * `Channels `_, the Django integration layer 26 | * `Daphne `_, the HTTP and Websocket termination server 27 | * `asgiref `_, the base ASGI library 28 | * `channels_redis `_, the Redis channel layer backend (optional) 29 | 30 | This documentation covers the system as a whole; individual release notes and 31 | instructions can be found in the individual repositories. 32 | 33 | .. _topics: 34 | 35 | Topics 36 | ------ 37 | 38 | .. toctree:: 39 | :maxdepth: 2 40 | 41 | introduction 42 | installation 43 | tutorial/index 44 | topics/consumers 45 | topics/routing 46 | topics/databases 47 | topics/channel_layers 48 | topics/sessions 49 | topics/authentication 50 | topics/security 51 | topics/testing 52 | topics/worker 53 | deploying 54 | topics/troubleshooting 55 | 56 | 57 | Reference 58 | --------- 59 | 60 | .. toctree:: 61 | :maxdepth: 2 62 | 63 | asgi 64 | channel_layer_spec 65 | community 66 | contributing 67 | support 68 | releases/index 69 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/consumers.py: -------------------------------------------------------------------------------- 1 | from channels.db import database_sync_to_async 2 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 3 | 4 | from .models import Message 5 | 6 | 7 | class LiveMessageConsumer(AsyncJsonWebsocketConsumer): 8 | async def connect(self): 9 | await self.channel_layer.group_add("live_message", self.channel_name) 10 | await self.accept() 11 | await self.send_current_state() 12 | 13 | async def disconnect(self, close_code): 14 | await self.channel_layer.group_discard("live_message", self.channel_name) 15 | 16 | @database_sync_to_async 17 | def _fetch_state(self): 18 | qs = Message.objects.order_by("-created") 19 | return { 20 | "count": qs.count(), 21 | "messages": list(qs.values("id", "title", "message")), 22 | } 23 | 24 | @database_sync_to_async 25 | def _create_message(self, title, text): 26 | Message.objects.create(title=title, message=text) 27 | 28 | @database_sync_to_async 29 | def _delete_message(self, msg_id): 30 | Message.objects.filter(id=msg_id).delete() 31 | 32 | async def receive_json(self, content): 33 | action = content.get("action", "create") 34 | 35 | if action == "create": 36 | title = content.get("title", "") 37 | text = content.get("message", "") 38 | await self._create_message(title=title, text=text) 39 | 40 | elif action == "delete": 41 | msg_id = content.get("id") 42 | await self._delete_message(msg_id) 43 | 44 | # After any action, rebroadcast current state 45 | await self.send_current_state() 46 | 47 | async def send_current_state(self): 48 | state = await self._fetch_state() 49 | await self.channel_layer.group_send( 50 | "live_message", {"type": "broadcast_message", **state} 51 | ) 52 | 53 | async def broadcast_message(self, event): 54 | await self.send_json( 55 | { 56 | "count": event["count"], 57 | "messages": event["messages"], 58 | } 59 | ) 60 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/static/sampleapp/css/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 20px 0; 3 | padding: 15px; 4 | border: 1px solid var(--border-color); 5 | box-shadow: 0 2px 4px rgba(169, 168, 168, 0.1); 6 | background: var(--body-bg); 7 | color: var(--body-fg); 8 | border-radius: 15px; 9 | } 10 | 11 | #heading { 12 | color: var(--heading-fg); 13 | margin-bottom: 20px; 14 | font-size: 1.2em; 15 | } 16 | 17 | .inputGroup { 18 | display: flex; 19 | flex-direction: column; 20 | gap: 10px; 21 | margin-bottom: 10px; 22 | } 23 | 24 | #msgTitle, 25 | #msgTextArea { 26 | padding: 8px; 27 | background: var(--input-bg); 28 | color: var(--input-fg); 29 | border: 1px solid var(--border-color); 30 | border-radius: 4px; 31 | font-size: 1em; 32 | font-family: inherit; 33 | } 34 | 35 | #sendBtn { 36 | padding: 6px 12px; 37 | background: var(--button-bg); 38 | color: var(--button-fg); 39 | border: 1px solid var(--border-color); 40 | cursor: pointer; 41 | border-radius: 4px; 42 | align-self: flex-start; 43 | } 44 | #sendBtn:hover { 45 | background: var(--button-hover-bg); 46 | } 47 | 48 | .stats { 49 | margin-top: 15px; 50 | margin-bottom: 10px; 51 | font-size: 0.9em; 52 | } 53 | 54 | #cardsContainer { 55 | display: flex; 56 | gap: 10px; 57 | } 58 | 59 | .messageCard { 60 | position: relative; 61 | height: max-content; 62 | background: var(--body-bg); 63 | border: 1px solid var(--border-color); 64 | border-radius: 8px; 65 | box-shadow: 0 2px 6px rgba(0,0,0,0.1); 66 | padding: 10px 12px 12px; 67 | overflow: hidden; 68 | display: flex; 69 | flex-direction: column; 70 | max-width: 25%; 71 | } 72 | 73 | .messageCard h3 { 74 | margin: 0 0 6px; 75 | font-size: 1.1em; 76 | font-weight: bold; 77 | color: var(--body-fg); 78 | padding-right: 20px; 79 | } 80 | 81 | .messageCard p { 82 | margin: 0; 83 | font-size: 0.95em; 84 | color: var(--body-fg); 85 | overflow-wrap: break-word; 86 | } 87 | 88 | .messageCard #deleteBtn { 89 | margin-top: 7px; 90 | background: transparent; 91 | padding: 0; 92 | color: red; 93 | border: none; 94 | cursor: pointer; 95 | line-height: 1; 96 | align-self: flex-end; 97 | } 98 | -------------------------------------------------------------------------------- /channels/testing/http.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import unquote, urlparse 2 | 3 | from channels.testing.application import ApplicationCommunicator 4 | 5 | 6 | class HttpCommunicator(ApplicationCommunicator): 7 | """ 8 | ApplicationCommunicator subclass that has HTTP shortcut methods. 9 | 10 | It will construct the scope for you, so you need to pass the application 11 | (uninstantiated) along with HTTP parameters. 12 | 13 | This does not support full chunking - for that, just use ApplicationCommunicator 14 | directly. 15 | """ 16 | 17 | def __init__(self, application, method, path, body=b"", headers=None): 18 | parsed = urlparse(path) 19 | self.scope = { 20 | "type": "http", 21 | "http_version": "1.1", 22 | "method": method.upper(), 23 | "path": unquote(parsed.path), 24 | "query_string": parsed.query.encode("utf-8"), 25 | "headers": headers or [], 26 | } 27 | assert isinstance(body, bytes) 28 | self.body = body 29 | self.sent_request = False 30 | super().__init__(application, self.scope) 31 | 32 | async def get_response(self, timeout=1): 33 | """ 34 | Get the application's response. Returns a dict with keys of 35 | "body", "headers" and "status". 36 | """ 37 | # If we've not sent the request yet, do so 38 | if not self.sent_request: 39 | self.sent_request = True 40 | await self.send_input({"type": "http.request", "body": self.body}) 41 | # Get the response start 42 | response_start = await self.receive_output(timeout) 43 | assert response_start["type"] == "http.response.start" 44 | # Get all body parts 45 | response_start["body"] = b"" 46 | while True: 47 | chunk = await self.receive_output(timeout) 48 | assert chunk["type"] == "http.response.body" 49 | assert isinstance(chunk["body"], bytes) 50 | response_start["body"] += chunk["body"] 51 | if not chunk.get("more_body", False): 52 | break 53 | # Return structured info 54 | del response_start["type"] 55 | response_start.setdefault("headers", []) 56 | return response_start 57 | -------------------------------------------------------------------------------- /docs/releases/3.0.3.rst: -------------------------------------------------------------------------------- 1 | 3.0.3 Release Notes 2 | =================== 3 | 4 | Channels 3.0.3 fixes a security issue in Channels 3.0.2 5 | 6 | CVE-2020-35681: Potential leakage of session identifiers using legacy ``AsgiHandler`` 7 | ------------------------------------------------------------------------------------- 8 | 9 | The legacy ``channels.http.AsgiHandler`` class, used for handling HTTP type 10 | requests in an ASGI environment prior to Django 3.0, did not correctly separate 11 | request scopes in Channels 3.0. In many cases this would result in a crash but, 12 | with correct timing responses could be sent to the wrong client, resulting in 13 | potential leakage of session identifiers and other sensitive data. 14 | 15 | This issue affects Channels 3.0.x before 3.0.3, and is resolved in Channels 16 | 3.0.3. 17 | 18 | Users of ``ProtocolTypeRouter`` not explicitly specifying the handler for the 19 | ``'http'`` key, or those explicitly using ``channels.http.AsgiHandler``, likely 20 | to support Django v2.2, are affected and should update immediately. 21 | 22 | Note that both an unspecified handler for the ``'http'`` key and using 23 | ``channels.http.AsgiHandler`` are deprecated, and will raise a warning, from 24 | Channels v3.0.0 25 | 26 | This issue affects only the legacy channels provided class, and not Django's 27 | similar ``ASGIHandler``, available from Django 3.0. It is recommended to update 28 | to Django 3.0+ and use the Django provided ``ASGIHandler``. 29 | 30 | A simplified ``asgi.py`` script will look like this: 31 | 32 | .. code-block:: python 33 | 34 | import os 35 | 36 | from django.core.asgi import get_asgi_application 37 | 38 | # Fetch Django ASGI application early to ensure AppRegistry is populated 39 | # before importing consumers and AuthMiddlewareStack that may import ORM 40 | # models. 41 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 42 | django_asgi_app = get_asgi_application() 43 | 44 | # Import other Channels classes and consumers here. 45 | from channels.routing import ProtocolTypeRouter, URLRouter 46 | 47 | application = ProtocolTypeRouter({ 48 | # Explicitly set 'http' key using Django's ASGI application. 49 | "http": django_asgi_app, 50 | ), 51 | }) 52 | 53 | Please see :doc:`/deploying` for a more complete example. 54 | -------------------------------------------------------------------------------- /channels/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import types 3 | 4 | 5 | def name_that_thing(thing): 6 | """ 7 | Returns either the function/class path or just the object's repr 8 | """ 9 | # Instance method 10 | if hasattr(thing, "im_class"): 11 | # Mocks will recurse im_class forever 12 | if hasattr(thing, "mock_calls"): 13 | return "" 14 | return name_that_thing(thing.im_class) + "." + thing.im_func.func_name 15 | # Other named thing 16 | if hasattr(thing, "__name__"): 17 | if hasattr(thing, "__class__") and not isinstance( 18 | thing, (types.FunctionType, types.MethodType) 19 | ): 20 | if thing.__class__ is not type and not issubclass(thing.__class__, type): 21 | return name_that_thing(thing.__class__) 22 | if hasattr(thing, "__self__"): 23 | return "%s.%s" % (thing.__self__.__module__, thing.__self__.__name__) 24 | if hasattr(thing, "__module__"): 25 | return "%s.%s" % (thing.__module__, thing.__name__) 26 | # Generic instance of a class 27 | if hasattr(thing, "__class__"): 28 | return name_that_thing(thing.__class__) 29 | return repr(thing) 30 | 31 | 32 | async def await_many_dispatch(consumer_callables, dispatch): 33 | """ 34 | Given a set of consumer callables, awaits on them all and passes results 35 | from them to the dispatch awaitable as they come in. 36 | """ 37 | # Call all callables, and ensure all return types are Futures 38 | tasks = [ 39 | asyncio.ensure_future(consumer_callable()) 40 | for consumer_callable in consumer_callables 41 | ] 42 | try: 43 | while True: 44 | # Wait for any of them to complete 45 | await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) 46 | # Find the completed one(s), yield results, and replace them 47 | for i, task in enumerate(tasks): 48 | if task.done(): 49 | result = task.result() 50 | await dispatch(result) 51 | tasks[i] = asyncio.ensure_future(consumer_callables[i]()) 52 | finally: 53 | # Make sure we clean up tasks on exit 54 | for task in tasks: 55 | task.cancel() 56 | try: 57 | await task 58 | except asyncio.CancelledError: 59 | pass 60 | -------------------------------------------------------------------------------- /tests/sample_project/sampleapp/static/sampleapp/js/scripts.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const countElement = document.getElementById('messageCount'); 3 | const container = document.getElementById('cardsContainer'); 4 | const titleInput = document.getElementById('msgTitle'); 5 | const textInput = document.getElementById('msgTextArea'); 6 | const sendBtn = document.getElementById('sendBtn'); 7 | 8 | const ws = initWebSocket(); 9 | 10 | function initWebSocket() { 11 | const wsPath = `ws://${window.location.host}/ws/message/`; 12 | const socket = new WebSocket(wsPath); 13 | 14 | window.websocketConnected = false; 15 | window.messageHandled = false; 16 | 17 | socket.onopen = () => { 18 | window.websocketConnected = true; 19 | console.log('WebSocket connected'); 20 | }; 21 | socket.onerror = err => console.error('WebSocket Error:', err); 22 | socket.onclose = () => console.warn('WebSocket closed'); 23 | socket.onmessage = handleMessage; 24 | 25 | return socket; 26 | } 27 | 28 | function handleMessage(e) { 29 | const data = JSON.parse(e.data); 30 | renderState(data.count, data.messages); 31 | window.messageHandled = true; 32 | } 33 | 34 | function renderState(count, messages) { 35 | countElement.textContent = count; 36 | container.innerHTML = ''; 37 | messages.forEach(msg => container.appendChild(createCard(msg))); 38 | } 39 | 40 | function createCard({ id, title, message }) { 41 | const card = document.createElement('div'); 42 | card.className = 'messageCard'; 43 | 44 | const h3 = document.createElement('h3'); 45 | h3.textContent = title; 46 | 47 | card.appendChild(h3); 48 | 49 | const p = document.createElement('p'); 50 | p.textContent = message; 51 | card.appendChild(p); 52 | 53 | const deleteBtn = document.createElement('button'); 54 | deleteBtn.id = 'deleteBtn'; 55 | deleteBtn.textContent = 'Delete'; 56 | deleteBtn.onclick = () => sendAction('delete', { id }); 57 | card.appendChild(deleteBtn); 58 | 59 | return card; 60 | } 61 | 62 | function sendAction(action, data = {}) { 63 | const payload = { action, ...data }; 64 | ws.send(JSON.stringify(payload)); 65 | } 66 | 67 | sendBtn.onclick = () => { 68 | const title = titleInput.value.trim(); 69 | const message = textInput.value.trim(); 70 | if (!title || !message) { 71 | return alert('Please enter both title and message.'); 72 | } 73 | sendAction('create', { title, message }); 74 | titleInput.value = ''; 75 | textInput.value = ''; 76 | }; 77 | })(); 78 | -------------------------------------------------------------------------------- /docs/asgi.rst: -------------------------------------------------------------------------------- 1 | ASGI 2 | ==== 3 | 4 | `ASGI `_, or the 5 | Asynchronous Server Gateway Interface, is the specification which 6 | Channels and Daphne are built upon, designed to untie Channels apps from a 7 | specific application server and provide a common way to write application 8 | and middleware code. 9 | 10 | It's a spiritual successor to WSGI, designed not only run in an asynchronous 11 | fashion via ``asyncio``, but also supporting multiple protocols. 12 | 13 | The full ASGI spec can be found at https://asgi.readthedocs.io 14 | 15 | 16 | Summary 17 | ------- 18 | 19 | ASGI is structured as a single asynchronous callable, which takes a dict ``scope`` 20 | and two callables ``receive`` and ``send``: 21 | 22 | .. code-block:: python 23 | 24 | async def application(scope, receive, send): 25 | event = await receive() 26 | ... 27 | await send({"type": "websocket.send", ...}) 28 | 29 | The ``scope`` dict defines the properties of a connection, like its remote IP (for 30 | HTTP) or username (for a chat protocol), and the lifetime of a connection. 31 | Applications are *instantiated* once per scope - so, for example, once per 32 | HTTP request, or once per open WebSocket connection. 33 | 34 | Scopes always have a ``type`` key, which tells you what kind of connection 35 | it is and what other keys to expect in the scope (and what sort of messages 36 | to expect). 37 | 38 | The ``receive`` awaitable provides events as dicts as they occur, and the 39 | ``send`` awaitable sends events back to the client in a similar dict format. 40 | 41 | A *protocol server* sits between the client and your application code, 42 | decoding the raw protocol into the scope and event dicts and encoding anything 43 | you send back down onto the protocol. 44 | 45 | 46 | Composability 47 | ------------- 48 | 49 | ASGI applications, like WSGI ones, are designed to be composable, and this 50 | includes Channels' routing and middleware components like ``ProtocolTypeRouter`` 51 | and ``SessionMiddleware``. These are just ASGI applications that take other 52 | ASGI applications as arguments, so you can pass around just one top-level 53 | application for a whole Django project and dispatch down to the right consumer 54 | based on what sort of connection you're handling. 55 | 56 | 57 | Protocol Specifications 58 | ----------------------- 59 | 60 | The basic ASGI spec only outlines the interface for an ASGI app - it does not 61 | specify how network protocols are encoded to and from scopes and event dicts. 62 | That's the job of protocol specifications: 63 | 64 | * HTTP and WebSocket: https://github.com/django/asgiref/blob/master/specs/www.rst 65 | -------------------------------------------------------------------------------- /docs/topics/security.rst: -------------------------------------------------------------------------------- 1 | Security 2 | ======== 3 | 4 | This covers basic security for protocols you're serving via Channels and 5 | helpers that we provide. 6 | 7 | 8 | WebSockets 9 | ---------- 10 | 11 | WebSockets start out life as a HTTP request, including all the cookies 12 | and headers, and so you can use the standard :doc:`/topics/authentication` 13 | code in order to grab current sessions and check user IDs. 14 | 15 | There is also a risk of cross-site request forgery (CSRF) with WebSockets though, 16 | as they can be initiated from any site on the internet to your domain, and will 17 | still have the user's cookies and session from your site. If you serve private 18 | data down the socket, you should restrict the sites which are allowed to open 19 | sockets to you. 20 | 21 | This is done via the ``channels.security.websocket`` package, and the two 22 | ASGI middlewares it contains, ``OriginValidator`` and 23 | ``AllowedHostsOriginValidator``. 24 | 25 | ``OriginValidator`` lets you restrict the valid options for the ``Origin`` 26 | header that is sent with every WebSocket to say where it comes from. Just wrap 27 | it around your WebSocket application code like this, and pass it a list of 28 | valid domains as the second argument. You can pass only a single domain (for example, 29 | ``.allowed-domain.com``) or a full origin, in the format ``scheme://domain[:port]`` 30 | (for example, ``http://allowed-domain.com:80``). Port is optional, but recommended: 31 | 32 | .. code-block:: python 33 | 34 | from channels.security.websocket import OriginValidator 35 | 36 | application = ProtocolTypeRouter({ 37 | 38 | "websocket": OriginValidator( 39 | AuthMiddlewareStack( 40 | URLRouter([ 41 | ... 42 | ]) 43 | ), 44 | [".goodsite.com", "http://.goodsite.com:80", "http://other.site.com"], 45 | ), 46 | }) 47 | 48 | Note: If you want to resolve any domain, then use the origin ``*``. 49 | 50 | 51 | Often, the set of domains you want to restrict to is the same as the Django 52 | ``ALLOWED_HOSTS`` setting, which performs a similar security check for the 53 | ``Host`` header, and so ``AllowedHostsOriginValidator`` lets you use this 54 | setting without having to re-declare the list: 55 | 56 | .. code-block:: python 57 | 58 | from channels.security.websocket import AllowedHostsOriginValidator 59 | 60 | application = ProtocolTypeRouter({ 61 | 62 | "websocket": AllowedHostsOriginValidator( 63 | AuthMiddlewareStack( 64 | URLRouter([ 65 | ... 66 | ]) 67 | ), 68 | ), 69 | }) 70 | 71 | ``AllowedHostsOriginValidator`` will also automatically allow local connections 72 | through if the site is in ``DEBUG`` mode, much like Django's host validation. 73 | -------------------------------------------------------------------------------- /tests/sample_project/config/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | BASE_DIR = Path(__file__).resolve().parent.parent 4 | 5 | SECRET_KEY = "Not_a_secret_key" 6 | 7 | DEBUG = True 8 | 9 | ALLOWED_HOSTS = [] 10 | 11 | INSTALLED_APPS = [ 12 | "daphne", 13 | "django.contrib.admin", 14 | "django.contrib.auth", 15 | "django.contrib.contenttypes", 16 | "django.contrib.sessions", 17 | "django.contrib.messages", 18 | "django.contrib.staticfiles", 19 | "tests.sample_project.sampleapp", 20 | "channels", 21 | ] 22 | 23 | MIDDLEWARE = [ 24 | "django.middleware.security.SecurityMiddleware", 25 | "django.contrib.sessions.middleware.SessionMiddleware", 26 | "django.middleware.common.CommonMiddleware", 27 | "django.middleware.csrf.CsrfViewMiddleware", 28 | "django.contrib.auth.middleware.AuthenticationMiddleware", 29 | "django.contrib.messages.middleware.MessageMiddleware", 30 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 31 | ] 32 | 33 | ROOT_URLCONF = "tests.sample_project.config.urls" 34 | 35 | TEMPLATES = [ 36 | { 37 | "BACKEND": "django.template.backends.django.DjangoTemplates", 38 | "DIRS": [], 39 | "APP_DIRS": True, 40 | "OPTIONS": { 41 | "context_processors": [ 42 | "django.template.context_processors.csrf", 43 | "django.template.context_processors.request", 44 | "django.contrib.auth.context_processors.auth", 45 | "django.contrib.messages.context_processors.messages", 46 | ], 47 | }, 48 | }, 49 | ] 50 | 51 | WSGI_APPLICATION = "tests.sample_project.config.wsgi.application" 52 | ASGI_APPLICATION = "tests.sample_project.config.asgi.application" 53 | 54 | CHANNEL_LAYERS = { 55 | "default": { 56 | "BACKEND": "channels.layers.InMemoryChannelLayer", 57 | }, 58 | } 59 | 60 | DATABASES = { 61 | "default": { 62 | "ENGINE": "django.db.backends.sqlite3", 63 | "NAME": BASE_DIR / "sampleapp/sampleapp.sqlite3", 64 | # Override Django’s default behaviour of using an in-memory database 65 | # in tests for SQLite, since that avoids connection.close() working. 66 | "TEST": {"NAME": "test_db.sqlite3"}, 67 | } 68 | } 69 | 70 | 71 | AUTH_PASSWORD_VALIDATORS = [ 72 | { 73 | "NAME": ( 74 | "django.contrib.auth.password_validation." 75 | "UserAttributeSimilarityValidator" 76 | ), 77 | }, 78 | { 79 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 80 | }, 81 | { 82 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 83 | }, 84 | { 85 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 86 | }, 87 | ] 88 | 89 | LANGUAGE_CODE = "en-us" 90 | 91 | TIME_ZONE = "UTC" 92 | 93 | USE_I18N = True 94 | 95 | USE_TZ = True 96 | 97 | STATIC_URL = "static/" 98 | 99 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 100 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Channels 2 | =============== 3 | 4 | .. image:: https://github.com/django/channels/workflows/Tests/badge.svg?branch=master 5 | :target: https://github.com/django/channels/actions 6 | 7 | .. image:: https://readthedocs.org/projects/channels/badge/?version=latest 8 | :target: https://channels.readthedocs.io/en/latest/?badge=latest 9 | 10 | .. image:: https://img.shields.io/pypi/v/channels.svg 11 | :target: https://pypi.python.org/pypi/channels 12 | 13 | .. image:: https://img.shields.io/pypi/l/channels.svg 14 | :target: https://pypi.python.org/pypi/channels 15 | 16 | Channels augments Django to bring WebSocket, long-poll HTTP, 17 | task offloading and other async support to your code, using familiar Django 18 | design patterns and a flexible underlying framework that lets you not only 19 | customize behaviours but also write support for your own protocols and needs. 20 | 21 | Documentation, installation and getting started instructions are at 22 | https://channels.readthedocs.io 23 | 24 | Channels is an official Django Project and as such has a deprecation policy. 25 | Details about what's deprecated or pending deprecation for each release is in 26 | the `release notes `_. 27 | 28 | Support can be obtained through several locations - see our 29 | `support docs `_ for more. 30 | 31 | You can install channels from PyPI as the ``channels`` package. 32 | See our `installation `_ 33 | and `tutorial `_ docs for more. 34 | 35 | Dependencies 36 | ------------ 37 | 38 | All Channels projects currently support Python 3.9 and up. ``channels`` is 39 | compatible with Django 4.2+. 40 | 41 | 42 | Contributing 43 | ------------ 44 | 45 | To learn more about contributing, please `read our contributing docs `_. 46 | 47 | 48 | Maintenance and Security 49 | ------------------------ 50 | 51 | To report security issues, please contact security@djangoproject.com. For GPG 52 | signatures and more security process information, see 53 | https://docs.djangoproject.com/en/dev/internals/security/. 54 | 55 | To report bugs or request new features, please open a new GitHub issue. For 56 | larger discussions, please post to the 57 | `django-developers mailing list `_. 58 | 59 | Maintenance is overseen by Carlton Gibson with help from others. It is a 60 | best-effort basis - we unfortunately can only dedicate guaranteed time to fixing 61 | security holes. 62 | 63 | If you are interested in joining the maintenance team, please 64 | `read more about contributing `_ 65 | and get in touch! 66 | 67 | 68 | Other Projects 69 | -------------- 70 | 71 | The Channels project is made up of several packages; the others are: 72 | 73 | * `Daphne `_, the HTTP and Websocket termination server 74 | * `channels_redis `_, the Redis channel backend 75 | * `asgiref `_, the base ASGI library/memory backend 76 | -------------------------------------------------------------------------------- /docs/releases/4.2.0.rst: -------------------------------------------------------------------------------- 1 | 4.2.0 Release Notes 2 | =================== 3 | 4 | Channels 4.2 introduces a couple of major but backwards-compatible 5 | changes, including most notably enhanced async support and fixing 6 | a long-standing bug where tests would try and close db connections 7 | and erroneously fail. 8 | 9 | Additionally, support has been added for Django 5.1. 10 | 11 | Enhanced Async Support 12 | ---------------------- 13 | 14 | Support for asynchronous consumers has been greatly improved. 15 | The documentation has been updated to reflect the async ORM 16 | features added in Django 4.2. A new `channels.db.aclose_old_connections` 17 | function has been added to easily close old database connections 18 | in async consumers. 19 | 20 | Warning: Channels now automatically closes connections in async 21 | consumers before a new connection, after receiving message (but 22 | before dispatching to consumer code), and after disconnecting. 23 | 24 | This change has been made to more closely align with Django's 25 | request/response cycle, and to help users avoid attempting 26 | to use stale/broken connections. 27 | 28 | Notably, Channels does NOT close connections before or after 29 | a consumer **sends** data. This is to avoid database churn and 30 | more closely align with user expectations. Instead, users are 31 | expected to call `aclose_old_connections` occasionally during 32 | long-lived async connections. 33 | 34 | Additionally, channels will automatically use the new async 35 | interface for sessions if Django 5.1 or greater is installed. 36 | This new interface can be slightly faster in certain cases 37 | as it does not always need to context-switch into synchronous 38 | execution. This does require a backwards-incompatible change to 39 | `channels.sessions.InstanceSessionWrapper`: the `save_session` 40 | function is now `async`. If `InstanceSessionWrapper` was being 41 | subclassed in some way (note that this class is an implementation 42 | detail and not documented) and `save_session` was being called 43 | or overridden, it will need to be updated to be called with `await` 44 | or defined as `async`, respectively. 45 | 46 | 47 | Bugfixes & Small Changes 48 | ------------------------ 49 | 50 | * InMemoryChannelLayer has been greatly improved: it now honors 51 | expiry times and per-channel capacities, has parallel sending 52 | and a safer internal implementation. Note: queue capacities 53 | can no longer be changed after a channel has been created. 54 | 55 | Thanks to @devkral (Alexander) 56 | 57 | * Database connections are no longer closed inside tests, which 58 | prevents erroneous "Cannot operate on a closed database" errors 59 | when running tets. 60 | 61 | Thanks to Jon Janzen. 62 | 63 | * An old import override and an unused deprecation message were removed 64 | 65 | Thanks to @sevdog (Devid) and Jon Janzen. 66 | 67 | * WebsocketCommunicator now has informative `assert` error messages 68 | 69 | Thanks to Karel Hovorka. 70 | 71 | * WebsocketConsumer now checks that "text" is not None before attempting 72 | to use it. This improves support for Hypercorn. 73 | 74 | Thanks to Joaquín Ossandon. 75 | 76 | * BaseChannelLayer now has prototypes on all its methods to improve 77 | the hit-rate for smart autocompleters when users need to author 78 | their own channel layer and need to implement all required methods. 79 | 80 | Thanks to Jophy Ye. 81 | -------------------------------------------------------------------------------- /docs/community.rst: -------------------------------------------------------------------------------- 1 | Community Projects 2 | ================== 3 | 4 | These projects from the community are developed on top of Channels: 5 | 6 | * Beatserver_, a periodic task scheduler for Django Channels. 7 | * EventStream_, a library to push data using the Server-Sent Events (SSE) protocol. 8 | * DjangoChannelsRestFramework_, a framework that provides DRF-like consumers for Channels. 9 | * Chanx_, a batteries-included WebSocket framework providing automatic message routing, Pydantic validation, 10 | type safety, AsyncAPI documentation generation, and comprehensive testing utilities for Django Channels, 11 | FastAPI, and ASGI applications. 12 | * DjangoLiveView_, is a framework for creating real-time, interactive web applications entirely in Python, inspired by Phoenix LiveView and Laravel Livewire. 13 | * ChannelsMultiplexer_, a JsonConsumer Multiplexer for Channels. 14 | * DjangoChannelsIRC_, an interface server and matching generic consumers for IRC. 15 | * Apollo_, a real-time polling application for corporate and academic environments. 16 | * DjangoChannelsJsonRpc_, a wrapper for the JSON-RPC protocol. 17 | * channels-demultiplexer_, a (de)multiplexer for ``AsyncJsonWebsocketConsumer`` consumers. 18 | * channels_postgres_, a Django Channels channel layer that uses PostgreSQL as its backing store. 19 | * channels-auth-token-middlewares_, Django REST framework token authentication middleware and 20 | * channels-valkey_, a Django Channels channel layer that uses valkey as its backing store. 21 | SimpleJWT_ middleware, such as QueryStringSimpleJWTAuthTokenMiddleware_ for WebSocket 22 | authentication. 23 | * types-channels_, type stubs for Channels from the `Python typeshed project`_. 24 | These stubs provide type checking support for mypy, PyCharm, and other type checkers. 25 | * django-channels-more-than-present_, is a Django app which adds "rooms" and presence notification capability. 26 | 27 | If you'd like to add your project, please submit a PR with a link and brief description. 28 | 29 | .. _Beatserver: https://github.com/rajasimon/beatserver 30 | .. _EventStream: https://github.com/fanout/django-eventstream 31 | .. _DjangoChannelsRestFramework: https://github.com/hishnash/djangochannelsrestframework 32 | .. _Chanx: https://github.com/huynguyengl99/chanx 33 | .. _DjangoLiveView: https://django-liveview.andros.dev/ 34 | .. _ChannelsMultiplexer: https://github.com/hishnash/channelsmultiplexer 35 | .. _DjangoChannelsIRC: https://github.com/AdvocatesInc/django-channels-irc 36 | .. _Apollo: https://github.com/maliesa96/apollo 37 | .. _DjangoChannelsJsonRpc: https://github.com/millerf/django-channels2-jsonrpc 38 | .. _django-channels-more-than-present: https://github.com/tanrax/django-channels-more-than-present 39 | .. _channels-demultiplexer: https://github.com/csdenboer/channels-demultiplexer 40 | .. _channels_postgres: https://github.com/danidee10/channels_postgres 41 | .. _channels-auth-token-middlewares: https://github.com/YegorDB/django-channels-auth-token-middlewares 42 | .. _channels-valkey: https://github.com/amirreza8002/channels_valkey 43 | .. _SimpleJWT: https://github.com/jazzband/djangorestframework-simplejwt 44 | .. _QueryStringSimpleJWTAuthTokenMiddleware: https://github.com/YegorDB/django-channels-auth-token-middlewares/tree/master/tutorial/drf#querystringsimplejwtauthtokenmiddleware 45 | .. _types-channels: https://pypi.org/project/types-channels/ 46 | .. _Python typeshed project: https://github.com/python/typeshed 47 | -------------------------------------------------------------------------------- /docs/topics/sessions.rst: -------------------------------------------------------------------------------- 1 | Sessions 2 | ======== 3 | 4 | Channels supports standard Django sessions using HTTP cookies for both HTTP 5 | and WebSocket. There are some caveats, however. 6 | 7 | 8 | Basic Usage 9 | ----------- 10 | 11 | The ``SessionMiddleware`` in Channels supports standard Django sessions, 12 | and like all middleware, should be wrapped around the ASGI application that 13 | needs the session information in its scope (for example, a ``URLRouter`` to 14 | apply it to a whole collection of consumers, or an individual consumer). 15 | 16 | ``SessionMiddleware`` requires ``CookieMiddleware`` to function. 17 | For convenience, these are also provided as a combined callable called 18 | ``SessionMiddlewareStack`` that includes both. All are importable from 19 | ``channels.session``. 20 | 21 | To use the middleware, wrap it around the appropriate level of consumer 22 | in your ``asgi.py``: 23 | 24 | .. code-block:: python 25 | 26 | from channels.routing import ProtocolTypeRouter, URLRouter 27 | from channels.security.websocket import AllowedHostsOriginValidator 28 | from channels.sessions import SessionMiddlewareStack 29 | 30 | from myapp import consumers 31 | 32 | application = ProtocolTypeRouter({ 33 | 34 | "websocket": AllowedHostsOriginValidator( 35 | SessionMiddlewareStack( 36 | URLRouter([ 37 | path("frontend/", consumers.AsyncChatConsumer.as_asgi()), 38 | ]) 39 | ) 40 | ), 41 | 42 | }) 43 | 44 | ``SessionMiddleware`` will only work on protocols that provide 45 | HTTP headers in their ``scope`` - by default, this is HTTP and WebSocket. 46 | 47 | To access the session, use ``self.scope["session"]`` in your consumer code: 48 | 49 | .. code-block:: python 50 | 51 | class ChatConsumer(WebsocketConsumer): 52 | 53 | def connect(self, event): 54 | self.scope["session"]["seed"] = random.randint(1, 1000) 55 | 56 | ``SessionMiddleware`` respects all the same Django settings as the default 57 | Django session framework, like ``SESSION_COOKIE_NAME`` and 58 | ``SESSION_COOKIE_DOMAIN``. 59 | 60 | 61 | Session Persistence 62 | ------------------- 63 | 64 | Within HTTP consumers or ASGI applications, session persistence works as you 65 | would expect from Django HTTP views - sessions are saved whenever you send 66 | a HTTP response that does not have status code ``500``. 67 | 68 | This is done by overriding any ``http.response.start`` messages to inject 69 | cookie headers into the response as you send it out. If you have set 70 | the ``SESSION_SAVE_EVERY_REQUEST`` setting to ``True``, it will save the 71 | session and send the cookie on every response, otherwise it will only save 72 | whenever the session is modified. 73 | 74 | If you are in a WebSocket consumer, however, the session is populated 75 | **but will never be saved automatically** - you must call 76 | ``scope["session"].save()`` (or the asynchronous version, 77 | ``scope["session"].asave()``) yourself whenever you want to persist a session 78 | to your session store. If you don't save, the session will still work correctly 79 | inside the consumer (as it's stored as an instance variable), but other 80 | connections or HTTP views won't be able to see the changes. 81 | 82 | .. note:: 83 | 84 | If you are in a long-polling HTTP consumer, you might want to save changes 85 | to the session before you send a response. If you want to do this, 86 | call ``scope["session"].save()``. 87 | -------------------------------------------------------------------------------- /docs/releases/3.0.0.rst: -------------------------------------------------------------------------------- 1 | 3.0.0 Release Notes 2 | =================== 3 | 4 | The Channels 3 update brings Channels into line with Django's own async ASGI 5 | support, introduced with Django 3.0. 6 | 7 | Channels now integrates with Django's async HTTP handling, whilst continuing to 8 | support WebSockets and other exciting consumer types. 9 | 10 | Channels 3 supports Django 3.x and beyond, as well continuing to support the 11 | Django 2.2 LTS. We will support Django 2.2 at least until the Django 3.2 LTS is 12 | released, yet may drop support after that, but before Django 2.2 is officially 13 | end-of-life. 14 | 15 | Likewise, we support Python 3.6+ but we **strongly advise** you to update to 16 | the latest Python versions, so 3.9 at the time of release. 17 | 18 | In both our Django and Python support, we reflect the reality that async Python 19 | and async Django are still both evolving rapidly. Many issues we see simply 20 | disappear if you update. Whatever you are doing with async, you should make 21 | sure you're on the latest versions. 22 | 23 | The highlight of this release is the upgrade to ASGI v3, which allows integration 24 | with Django's ASGI support. There are also two additional deprecations that you 25 | will need to deal with if you are updating an existing application. 26 | 27 | 28 | Update to ASGI 3 29 | ---------------- 30 | 31 | * Consumers are now ASGI 3 *single-callables* with the signature:: 32 | 33 | application(scope, receive, send) 34 | 35 | For generic consumers this change should be largely transparent, but you will 36 | need to update ``__init__()`` (no longer taking the scope) and ``__call__()`` 37 | (now taking the scope) **if you implemented these yourself**. 38 | 39 | * Consumers now have an ``as_asgi()`` class method you need to call when 40 | setting up your routing:: 41 | 42 | websocket_urlpatterns = [ 43 | re_path(r'ws/chat/(?P\w+)/$', consumers.ChatConsumer.as_asgi()), 44 | ] 45 | 46 | This returns an ASGI application that will instantiate the consumer 47 | per-request. It's similar to Django's ``as_view()``, which serves the same purpose. You 48 | can pass in keyword arguments for initialization if your consumer requires them. 49 | 50 | * Middleware will also need to be updated to the ASGI v3 signature. The 51 | ``channels.middleware.BaseMiddleware`` class is simplified, and available as 52 | an example. You probably don't need to actually subclass it under ASGI 3. 53 | 54 | Deprecations 55 | ------------ 56 | 57 | * Using ``ProtocolTypeRouter`` without an explicit ``"http"`` key is now 58 | deprecated. 59 | 60 | Following Django conventions, your entry point script should be named 61 | ``asgi.py``, and you should use Django's ``get_asgi_application()``, that is 62 | used by Django's default ``asgi.py`` template to route the ``"http"`` 63 | handler:: 64 | 65 | from django.core.asgi import get_asgi_application 66 | 67 | application = ProtocolTypeRouter({ 68 | "http": get_asgi_application(), 69 | # Other protocols here. 70 | }) 71 | 72 | Once the deprecation is removed, when we drop support for Django 2.2, not 73 | specifying an ``"http"`` key will mean that your application will not handle 74 | HTTP requests. 75 | 76 | * The Channels built-in HTTP protocol ``AsgiHandler`` is also deprecated. You 77 | should update to Django 3.0 or higher and use Django's 78 | ``get_asgi_application()``. Channel's ``AsgiHandler`` will be removed when we 79 | drop support for Django 2.2. 80 | -------------------------------------------------------------------------------- /channels/generic/http.py: -------------------------------------------------------------------------------- 1 | from channels.consumer import AsyncConsumer 2 | 3 | from ..db import aclose_old_connections 4 | from ..exceptions import StopConsumer 5 | 6 | 7 | class AsyncHttpConsumer(AsyncConsumer): 8 | """ 9 | Async HTTP consumer. Provides basic primitives for building asynchronous 10 | HTTP endpoints. 11 | """ 12 | 13 | def __init__(self, *args, **kwargs): 14 | self.body = [] 15 | 16 | async def send_headers(self, *, status=200, headers=None): 17 | """ 18 | Sets the HTTP response status and headers. Headers may be provided as 19 | a list of tuples or as a dictionary. 20 | 21 | Note that the ASGI spec requires that the protocol server only starts 22 | sending the response to the client after ``self.send_body`` has been 23 | called the first time. 24 | """ 25 | if headers is None: 26 | headers = [] 27 | elif isinstance(headers, dict): 28 | headers = list(headers.items()) 29 | 30 | await self.send( 31 | {"type": "http.response.start", "status": status, "headers": headers} 32 | ) 33 | 34 | async def send_body(self, body, *, more_body=False): 35 | """ 36 | Sends a response body to the client. The method expects a bytestring. 37 | 38 | Set ``more_body=True`` if you want to send more body content later. 39 | The default behavior closes the response, and further messages on 40 | the channel will be ignored. 41 | """ 42 | assert isinstance(body, bytes), "Body is not bytes" 43 | await self.send( 44 | {"type": "http.response.body", "body": body, "more_body": more_body} 45 | ) 46 | 47 | async def send_response(self, status, body, **kwargs): 48 | """ 49 | Sends a response to the client. This is a thin wrapper over 50 | ``self.send_headers`` and ``self.send_body``, and everything said 51 | above applies here as well. This method may only be called once. 52 | """ 53 | await self.send_headers(status=status, **kwargs) 54 | await self.send_body(body) 55 | 56 | async def handle(self, body): 57 | """ 58 | Receives the request body as a bytestring. Response may be composed 59 | using the ``self.send*`` methods; the return value of this method is 60 | thrown away. 61 | """ 62 | raise NotImplementedError( 63 | "Subclasses of AsyncHttpConsumer must provide a handle() method." 64 | ) 65 | 66 | async def disconnect(self): 67 | """ 68 | Overrideable place to run disconnect handling. Do not send anything 69 | from here. 70 | """ 71 | pass 72 | 73 | async def http_request(self, message): 74 | """ 75 | Async entrypoint - concatenates body fragments and hands off control 76 | to ``self.handle`` when the body has been completely received. 77 | """ 78 | if "body" in message: 79 | self.body.append(message["body"]) 80 | if not message.get("more_body"): 81 | try: 82 | await self.handle(b"".join(self.body)) 83 | finally: 84 | await self.disconnect() 85 | raise StopConsumer() 86 | 87 | async def http_disconnect(self, message): 88 | """ 89 | Let the user do their cleanup and close the consumer. 90 | """ 91 | await self.disconnect() 92 | await aclose_old_connections() 93 | raise StopConsumer() 94 | -------------------------------------------------------------------------------- /channels/testing/live.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from daphne.testing import DaphneProcess 4 | from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.db import connections 7 | from django.db.backends.base.creation import TEST_DATABASE_PREFIX 8 | from django.test.testcases import TransactionTestCase 9 | from django.test.utils import modify_settings 10 | 11 | from channels.routing import get_default_application 12 | 13 | 14 | def make_application(*, static_wrapper): 15 | # Module-level function for pickle-ability 16 | application = get_default_application() 17 | if static_wrapper is not None: 18 | application = static_wrapper(application) 19 | return application 20 | 21 | 22 | def set_database_connection(): 23 | from django.conf import settings 24 | 25 | test_db_name = settings.DATABASES["default"]["TEST"]["NAME"] 26 | if not test_db_name: 27 | test_db_name = TEST_DATABASE_PREFIX + settings.DATABASES["default"]["NAME"] 28 | settings.DATABASES["default"]["NAME"] = test_db_name 29 | 30 | 31 | class ChannelsLiveServerTestCase(TransactionTestCase): 32 | """ 33 | Does basically the same as TransactionTestCase but also launches a 34 | live Daphne server in a separate process, so 35 | that the tests may use another test framework, such as Selenium, 36 | instead of the built-in dummy client. 37 | """ 38 | 39 | host = "localhost" 40 | ProtocolServerProcess = DaphneProcess 41 | static_wrapper = ASGIStaticFilesHandler 42 | serve_static = True 43 | 44 | @property 45 | def live_server_url(self): 46 | return "http://%s:%s" % (self.host, self._port) 47 | 48 | @property 49 | def live_server_ws_url(self): 50 | return "ws://%s:%s" % (self.host, self._port) 51 | 52 | @classmethod 53 | def setUpClass(cls): 54 | for connection in connections.all(): 55 | if cls._is_in_memory_db(connection): 56 | raise ImproperlyConfigured( 57 | "ChannelLiveServerTestCase can not be used with in memory databases" 58 | ) 59 | 60 | super().setUpClass() 61 | 62 | cls._live_server_modified_settings = modify_settings( 63 | ALLOWED_HOSTS={"append": cls.host} 64 | ) 65 | cls._live_server_modified_settings.enable() 66 | 67 | get_application = partial( 68 | make_application, 69 | static_wrapper=cls.static_wrapper if cls.serve_static else None, 70 | ) 71 | cls._server_process = cls.ProtocolServerProcess( 72 | cls.host, 73 | get_application, 74 | setup=set_database_connection, 75 | ) 76 | cls._server_process.start() 77 | while True: 78 | if not cls._server_process.ready.wait(timeout=1): 79 | if cls._server_process.is_alive(): 80 | continue 81 | raise RuntimeError("Server stopped") from None 82 | break 83 | cls._port = cls._server_process.port.value 84 | 85 | @classmethod 86 | def tearDownClass(cls): 87 | cls._server_process.terminate() 88 | cls._server_process.join() 89 | cls._live_server_modified_settings.disable() 90 | super().tearDownClass() 91 | 92 | @classmethod 93 | def _is_in_memory_db(cls, connection): 94 | """ 95 | Check if DatabaseWrapper holds in memory database. 96 | """ 97 | if connection.vendor == "sqlite": 98 | return connection.is_in_memory_db() 99 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Channels is available on PyPI - to install it run: 5 | 6 | .. code-block:: sh 7 | 8 | python -m pip install -U 'channels[daphne]' 9 | 10 | This will install Channels together with the Daphne ASGI application server. If 11 | you wish to use a different application server you can ``pip install channels``, 12 | without the optional ``daphne`` add-on. 13 | 14 | Once that's done, you should add ``daphne`` to the beginning of your 15 | ``INSTALLED_APPS`` setting: 16 | 17 | .. code-block:: python 18 | 19 | INSTALLED_APPS = ( 20 | "daphne", 21 | "django.contrib.auth", 22 | "django.contrib.contenttypes", 23 | "django.contrib.sessions", 24 | "django.contrib.sites", 25 | ... 26 | ) 27 | 28 | This will install the Daphne's ASGI version of the ``runserver`` management 29 | command. 30 | 31 | You can also add ``"channels"`` for Channel's ``runworker`` command. 32 | 33 | Then, adjust your project's ``asgi.py`` file, e.g. ``myproject/asgi.py``, to 34 | wrap the Django ASGI application:: 35 | 36 | import os 37 | 38 | from channels.routing import ProtocolTypeRouter 39 | from django.core.asgi import get_asgi_application 40 | 41 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 42 | # Initialize Django ASGI application early to ensure the AppRegistry 43 | # is populated before importing code that may import ORM models. 44 | django_asgi_app = get_asgi_application() 45 | 46 | application = ProtocolTypeRouter({ 47 | "http": django_asgi_app, 48 | # Just HTTP for now. (We can add other protocols later.) 49 | }) 50 | 51 | And finally, set your ``ASGI_APPLICATION`` setting to point to that routing 52 | object as your root application: 53 | 54 | .. code-block:: python 55 | 56 | ASGI_APPLICATION = "myproject.asgi.application" 57 | 58 | That's it! Once enabled, ``daphne`` will integrate itself into Django and 59 | take control of the ``runserver`` command. See :doc:`introduction` for more. 60 | 61 | .. note:: 62 | 63 | Please be wary of any other third-party apps that require an overloaded or 64 | replacement ``runserver`` command. Daphne provides a separate 65 | ``runserver`` command and may conflict with it. An example 66 | of such a conflict is with `whitenoise.runserver_nostatic `_ 67 | from `whitenoise `_. In order to 68 | solve such issues, make sure ``daphne`` is at the top of your ``INSTALLED_APPS`` 69 | or remove the offending app altogether. 70 | 71 | 72 | Type checking support 73 | --------------------- 74 | 75 | If you want type checking support, you can install the type stubs: 76 | 77 | .. code-block:: sh 78 | 79 | python -m pip install types-channels 80 | 81 | Or install channels with type support using the ``types`` extra: 82 | 83 | .. code-block:: sh 84 | 85 | python -m pip install 'channels[types]' 86 | 87 | 88 | Installing the latest development version 89 | ----------------------------------------- 90 | 91 | To install the latest version of Channels, clone the repo, change to the repo directory, 92 | and pip install it into your current virtual 93 | environment: 94 | 95 | .. code-block:: sh 96 | 97 | $ git clone git@github.com:django/channels.git 98 | $ cd channels 99 | $ 100 | (environment) $ pip install -e . # the dot specifies the current repo 101 | -------------------------------------------------------------------------------- /docs/topics/worker.rst: -------------------------------------------------------------------------------- 1 | Worker and Background Tasks 2 | =========================== 3 | 4 | While :doc:`channel layers ` are primarily designed for 5 | communicating between different instances of ASGI applications, they can also 6 | be used to offload work to a set of worker servers listening on fixed channel 7 | names, as a simple, very-low-latency task queue. 8 | 9 | .. note:: 10 | 11 | The worker/background tasks system in Channels is simple and very fast, 12 | and achieves this by not having some features you may find useful, such as 13 | retries or return values. 14 | 15 | We recommend you use it for work that does not need guarantees around 16 | being complete (at-most-once delivery), and for work that needs more 17 | guarantees, look into a separate dedicated task queue. 18 | 19 | This feature does not work with the in-memory channel layer. 20 | 21 | Setting up background tasks works in two parts - sending the events, and then 22 | setting up the consumers to receive and process the events. 23 | 24 | 25 | Sending 26 | ------- 27 | 28 | To send an event, just send it to a fixed channel name. For example, let's say 29 | we want a background process that pre-caches thumbnails: 30 | 31 | .. code-block:: python 32 | 33 | # Inside a consumer 34 | self.channel_layer.send( 35 | "thumbnails-generate", 36 | { 37 | "type": "generate", 38 | "id": 123456789, 39 | }, 40 | ) 41 | 42 | Note that the event you send **must** have a ``type`` key, even if only one 43 | type of message is being sent over the channel, as it will turn into an event 44 | a consumer has to handle. 45 | 46 | Also remember that if you are sending the event from a synchronous environment, 47 | you have to use the ``asgiref.sync.async_to_sync`` wrapper as specified in 48 | :doc:`channel layers `. 49 | 50 | Receiving and Consumers 51 | ----------------------- 52 | 53 | Channels will present incoming worker tasks to you as events inside a scope 54 | with a ``type`` of ``channel``, and a ``channel`` key matching the channel 55 | name. We recommend you use ProtocolTypeRouter and ChannelNameRouter (see 56 | :doc:`/topics/routing` for more) to arrange your consumers: 57 | 58 | .. code-block:: python 59 | 60 | application = ProtocolTypeRouter({ 61 | ... 62 | "channel": ChannelNameRouter({ 63 | "thumbnails-generate": consumers.GenerateConsumer.as_asgi(), 64 | "thumbnails-delete": consumers.DeleteConsumer.as_asgi(), 65 | }), 66 | }) 67 | 68 | You'll be specifying the ``type`` values of the individual events yourself 69 | when you send them, so decide what your names are going to be and write 70 | consumers to match. For example, here's a basic consumer that expects to 71 | receive an event with ``type`` ``test.print``, and a ``text`` value containing 72 | the text to print: 73 | 74 | .. code-block:: python 75 | 76 | class PrintConsumer(SyncConsumer): 77 | def test_print(self, message): 78 | print("Test: " + message["text"]) 79 | 80 | Once you've hooked up the consumers, all you need to do is run a process that 81 | will handle them. In lieu of a protocol server - as there are no connections 82 | involved here - Channels instead provides you this with the ``runworker`` 83 | command: 84 | 85 | .. code-block:: text 86 | 87 | python manage.py runworker thumbnails-generate thumbnails-delete 88 | 89 | Note that ``runworker`` will only listen to the channels you pass it on the 90 | command line. If you do not include a channel, or forget to run the worker, 91 | your events will not be received and acted upon. 92 | -------------------------------------------------------------------------------- /tests/sample_project/tests/test_selenium.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | from selenium.webdriver.support.ui import WebDriverWait 3 | 4 | from channels.testing import ChannelsLiveServerTestCase 5 | from tests.sample_project.sampleapp.models import Message 6 | 7 | from .selenium_mixin import SeleniumMixin 8 | 9 | 10 | class TestSampleApp(SeleniumMixin, ChannelsLiveServerTestCase): 11 | serve_static = True 12 | 13 | def setUp(self): 14 | super().setUp() 15 | self.login() 16 | self.open_admin_message_page() 17 | 18 | def open_admin_message_page(self): 19 | self.open("/admin/sampleapp/message/") 20 | self.wait_for_websocket_connection() 21 | 22 | def _create_message(self, title="Test Title", message="Test Message"): 23 | return Message.objects.create(title=title, message=message) 24 | 25 | def _wait_for_exact_text(self, by, locator, exact, timeout=2): 26 | WebDriverWait(self.web_driver, timeout).until( 27 | lambda driver: driver.find_element(by, locator).text == str(exact) 28 | ) 29 | 30 | def test_real_time_create_message(self): 31 | self.web_driver.switch_to.new_window("tab") 32 | tabs = self.web_driver.window_handles 33 | self.web_driver.switch_to.window(tabs[1]) 34 | 35 | self.open_admin_message_page() 36 | titleInput = self.find_element(By.ID, "msgTitle") 37 | self.assertIsNotNone(titleInput, "Title input should be present") 38 | messageInput = self.find_element(By.ID, "msgTextArea") 39 | self.assertIsNotNone(messageInput, "Message input should be present") 40 | addMessageButton = self.find_element(By.ID, "sendBtn") 41 | self.assertIsNotNone(addMessageButton, "Send button should be present") 42 | titleInput.send_keys("Test Title") 43 | messageInput.send_keys("Test Message") 44 | addMessageButton.click() 45 | self._wait_for_exact_text(By.ID, "messageCount", 1) 46 | messageCount = self.find_element(By.ID, "messageCount") 47 | self.assertIsNotNone(messageCount, "Message count should be present") 48 | self.assertEqual(messageCount.text, "1") 49 | 50 | self.web_driver.switch_to.window(tabs[0]) 51 | messageCount = self.find_element(By.ID, "messageCount") 52 | self.assertIsNotNone(messageCount, "Message count should be present") 53 | self.assertEqual(messageCount.text, "1") 54 | 55 | def test_real_time_delete_message(self): 56 | self._create_message() 57 | self.web_driver.refresh() 58 | self.wait_for_websocket_message_handled() 59 | 60 | messageCount = self.find_element(By.ID, "messageCount") 61 | self.assertIsNotNone(messageCount, "Message count should be present") 62 | self.assertEqual(messageCount.text, "1") 63 | 64 | self.web_driver.switch_to.new_window("tab") 65 | tabs = self.web_driver.window_handles 66 | self.web_driver.switch_to.window(tabs[1]) 67 | 68 | self.open_admin_message_page() 69 | deleteButton = self.find_element(By.ID, "deleteBtn") 70 | self.assertIsNotNone(deleteButton, "Delete button should be present") 71 | deleteButton.click() 72 | self._wait_for_exact_text(By.ID, "messageCount", 0) 73 | 74 | messageCount = self.find_element(By.ID, "messageCount") 75 | self.assertIsNotNone(messageCount, "Message count should be present") 76 | self.assertEqual(messageCount.text, "0") 77 | 78 | self.web_driver.switch_to.window(tabs[0]) 79 | messageCount = self.find_element(By.ID, "messageCount") 80 | self.assertIsNotNone(messageCount, "Message count should be present") 81 | self.assertEqual(messageCount.text, "0") 82 | -------------------------------------------------------------------------------- /docs/topics/databases.rst: -------------------------------------------------------------------------------- 1 | Database Access 2 | =============== 3 | 4 | The Django ORM is a synchronous piece of code, and so if you want to access 5 | it from asynchronous code you need to do special handling to make sure its 6 | connections are closed properly. 7 | 8 | If you're using ``SyncConsumer``, or anything based on it - like 9 | ``JsonWebsocketConsumer`` - you don't need to do anything special, as all your 10 | code is already run in a synchronous mode and Channels will do the cleanup 11 | for you as part of the ``SyncConsumer`` code. 12 | 13 | If you are writing asynchronous code, however, you will need to call 14 | database methods in a safe, synchronous context, using ``database_sync_to_async`` 15 | or by using the asynchronous methods prefixed with ``a`` like ``Model.objects.aget()``. 16 | 17 | 18 | Database Connections 19 | -------------------- 20 | 21 | Channels can potentially open a lot more database connections than you may be used to if you are using threaded consumers (synchronous ones) - it can open up to one connection per thread. 22 | 23 | If you wish to control the maximum number of threads used, set the 24 | ``ASGI_THREADS`` environment variable to the maximum number you wish to allow. 25 | By default, the number of threads is set to "the number of CPUs * 5" for 26 | Python 3.7 and below, and `min(32, os.cpu_count() + 4)` for Python 3.8+. 27 | 28 | To avoid having too many threads idling in connections, you can instead rewrite your code to use async consumers and only dip into threads when you need to use Django's ORM (using ``database_sync_to_async``). 29 | 30 | When using async consumers Channels will automatically call Django's ``close_old_connections`` method when a new connection is started, when a connection is closed, and whenever anything is received from the client. 31 | This mirrors Django's logic for closing old connections at the start and end of a request, to the extent possible. Connections are *not* automatically closed when sending data from a consumer since Channels has no way 32 | to determine if this is a one-off send (and connections could be closed) or a series of sends (in which closing connections would kill performance). Instead, if you have a long-lived async consumer you should 33 | periodically call ``aclose_old_connections`` (see below). 34 | 35 | 36 | database_sync_to_async 37 | ---------------------- 38 | 39 | ``channels.db.database_sync_to_async`` is a version of ``asgiref.sync.sync_to_async`` 40 | that also cleans up database connections on exit. 41 | 42 | To use it, write your ORM queries in a separate function or method, and then 43 | call it with ``database_sync_to_async`` like so: 44 | 45 | .. code-block:: python 46 | 47 | from channels.db import database_sync_to_async 48 | 49 | async def connect(self): 50 | self.username = await database_sync_to_async(get_name)() 51 | 52 | def get_name(self): 53 | return User.objects.all()[0].name 54 | 55 | You can also use it as a decorator: 56 | 57 | .. code-block:: python 58 | 59 | from channels.db import database_sync_to_async 60 | 61 | async def connect(self): 62 | self.username = await get_name() 63 | 64 | @database_sync_to_async 65 | def get_name(self): 66 | return User.objects.all()[0].name 67 | 68 | aclose_old_connections 69 | ---------------------- 70 | 71 | ``channels.db.aclose_old_connections`` is an async wrapper around Django's 72 | ``close_old_connections``. When using a long-lived ``AsyncConsumer`` that 73 | calls the Django ORM it is important to call this function periodically. 74 | 75 | Preferrably, this function should be called before making the first query 76 | in a while. For example, it should be called if the Consumer is woken up 77 | by a channels layer event and needs to make a few ORM queries to determine 78 | what to send to the client. This function should be called *before* making 79 | those queries. Calling this function more than necessary is not necessarily 80 | a bad thing, but it does require a context switch to synchronous code and 81 | so incurs a small penalty. 82 | -------------------------------------------------------------------------------- /tests/security/test_websocket.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from channels.generic.websocket import AsyncWebsocketConsumer 4 | from channels.security.websocket import OriginValidator 5 | from channels.testing import WebsocketCommunicator 6 | 7 | 8 | @pytest.mark.django_db(transaction=True) 9 | @pytest.mark.asyncio 10 | async def test_origin_validator(): 11 | """ 12 | Tests that OriginValidator correctly allows/denies connections. 13 | """ 14 | # Make our test application 15 | application = OriginValidator(AsyncWebsocketConsumer(), ["allowed-domain.com"]) 16 | # Test a normal connection 17 | communicator = WebsocketCommunicator( 18 | application, "/", headers=[(b"origin", b"http://allowed-domain.com")] 19 | ) 20 | connected, _ = await communicator.connect() 21 | assert connected 22 | await communicator.disconnect() 23 | # Test a bad connection 24 | communicator = WebsocketCommunicator( 25 | application, "/", headers=[(b"origin", b"http://bad-domain.com")] 26 | ) 27 | connected, _ = await communicator.connect() 28 | assert not connected 29 | await communicator.disconnect() 30 | # Make our test application, bad pattern 31 | application = OriginValidator(AsyncWebsocketConsumer(), ["*.allowed-domain.com"]) 32 | # Test a bad connection 33 | communicator = WebsocketCommunicator( 34 | application, "/", headers=[(b"origin", b"http://allowed-domain.com")] 35 | ) 36 | connected, _ = await communicator.connect() 37 | assert not connected 38 | await communicator.disconnect() 39 | # Make our test application, good pattern 40 | application = OriginValidator(AsyncWebsocketConsumer(), [".allowed-domain.com"]) 41 | # Test a normal connection 42 | communicator = WebsocketCommunicator( 43 | application, "/", headers=[(b"origin", b"http://www.allowed-domain.com")] 44 | ) 45 | connected, _ = await communicator.connect() 46 | assert connected 47 | await communicator.disconnect() 48 | # Make our test application, with scheme://domain[:port] for http 49 | application = OriginValidator( 50 | AsyncWebsocketConsumer(), ["http://allowed-domain.com"] 51 | ) 52 | # Test a normal connection 53 | communicator = WebsocketCommunicator( 54 | application, "/", headers=[(b"origin", b"http://allowed-domain.com")] 55 | ) 56 | connected, _ = await communicator.connect() 57 | assert connected 58 | await communicator.disconnect() 59 | # Test a bad connection 60 | communicator = WebsocketCommunicator( 61 | application, "/", headers=[(b"origin", b"https://bad-domain.com:443")] 62 | ) 63 | connected, _ = await communicator.connect() 64 | assert not connected 65 | await communicator.disconnect() 66 | # Make our test application, with all hosts allowed 67 | application = OriginValidator(AsyncWebsocketConsumer(), ["*"]) 68 | # Test a connection without any headers 69 | communicator = WebsocketCommunicator(application, "/", headers=[]) 70 | connected, _ = await communicator.connect() 71 | assert connected 72 | await communicator.disconnect() 73 | # Make our test application, with no hosts allowed 74 | application = OriginValidator(AsyncWebsocketConsumer(), []) 75 | # Test a connection without any headers 76 | communicator = WebsocketCommunicator(application, "/", headers=[]) 77 | connected, _ = await communicator.connect() 78 | assert not connected 79 | await communicator.disconnect() 80 | # Test bug with subdomain and empty origin header 81 | application = OriginValidator(AsyncWebsocketConsumer(), [".allowed-domain.com"]) 82 | communicator = WebsocketCommunicator(application, "/", headers=[(b"origin", b"")]) 83 | connected, _ = await communicator.connect() 84 | assert not connected 85 | await communicator.disconnect() 86 | # Test bug with subdomain and invalid origin header 87 | application = OriginValidator(AsyncWebsocketConsumer(), [".allowed-domain.com"]) 88 | communicator = WebsocketCommunicator( 89 | application, "/", headers=[(b"origin", b"something-invalid")] 90 | ) 91 | connected, _ = await communicator.connect() 92 | assert not connected 93 | await communicator.disconnect() 94 | -------------------------------------------------------------------------------- /docs/tutorial/part_3.rst: -------------------------------------------------------------------------------- 1 | Tutorial Part 3: Rewrite Chat Server as Asynchronous 2 | ==================================================== 3 | 4 | This tutorial begins where :doc:`Tutorial 2 ` left off. 5 | We'll rewrite the consumer code to be asynchronous rather than synchronous 6 | to improve its performance. 7 | 8 | Rewrite the consumer to be asynchronous 9 | --------------------------------------- 10 | 11 | The ``ChatConsumer`` that we have written is currently synchronous. Synchronous 12 | consumers are convenient because they can call regular synchronous I/O functions 13 | such as those that access Django models without writing special code. However 14 | asynchronous consumers can provide a higher level of performance since they 15 | don't need to create additional threads when handling requests. 16 | 17 | ``ChatConsumer`` only uses async-native libraries (Channels and the channel layer) 18 | and in particular it does not access synchronous code. Therefore it can 19 | be rewritten to be asynchronous without complications. 20 | 21 | .. note:: 22 | Even if ``ChatConsumer`` *did* access Django models or synchronous code it 23 | would still be possible to rewrite it as asynchronous. Utilities like 24 | :ref:`asgiref.sync.sync_to_async ` and 25 | :doc:`channels.db.database_sync_to_async ` can be 26 | used to call synchronous code from an asynchronous consumer. The performance 27 | gains however would be less than if it only used async-native libraries. Django 28 | models include methods prefixed with ``a`` that can be used safely from async 29 | contexts, provided that 30 | :doc:`channels.db.aclose_old_connections ` is called 31 | occasionally. 32 | 33 | Let's rewrite ``ChatConsumer`` to be asynchronous. 34 | Put the following code in ``chat/consumers.py``: 35 | 36 | .. code-block:: python 37 | 38 | # chat/consumers.py 39 | import json 40 | 41 | from channels.generic.websocket import AsyncWebsocketConsumer 42 | 43 | 44 | class ChatConsumer(AsyncWebsocketConsumer): 45 | async def connect(self): 46 | self.room_name = self.scope["url_route"]["kwargs"]["room_name"] 47 | self.room_group_name = f"chat_{self.room_name}" 48 | 49 | # Join room group 50 | await self.channel_layer.group_add(self.room_group_name, self.channel_name) 51 | 52 | await self.accept() 53 | 54 | async def disconnect(self, close_code): 55 | # Leave room group 56 | await self.channel_layer.group_discard(self.room_group_name, self.channel_name) 57 | 58 | # Receive message from WebSocket 59 | async def receive(self, text_data): 60 | text_data_json = json.loads(text_data) 61 | message = text_data_json["message"] 62 | 63 | # Send message to room group 64 | await self.channel_layer.group_send( 65 | self.room_group_name, {"type": "chat.message", "message": message} 66 | ) 67 | 68 | # Receive message from room group 69 | async def chat_message(self, event): 70 | message = event["message"] 71 | 72 | # Send message to WebSocket 73 | await self.send(text_data=json.dumps({"message": message})) 74 | 75 | This new code is for ChatConsumer is very similar to the original code, with the following differences: 76 | 77 | * ``ChatConsumer`` now inherits from ``AsyncWebsocketConsumer`` rather than 78 | ``WebsocketConsumer``. 79 | * All methods are ``async def`` rather than just ``def``. 80 | * ``await`` is used to call asynchronous functions that perform I/O. 81 | * ``async_to_sync`` is no longer needed when calling methods on the channel layer. 82 | 83 | Let's verify that the consumer for the ``/ws/chat/ROOM_NAME/`` path still works. 84 | To start the Channels development server, run the following command: 85 | 86 | .. code-block:: sh 87 | 88 | $ python3 manage.py runserver 89 | 90 | Open a browser tab to the room page at http://127.0.0.1:8000/chat/lobby/. 91 | Open a second browser tab to the same room page. 92 | 93 | In the second browser tab, type the message "hello" and press enter. You should 94 | now see "hello" echoed in the chat log in both the second browser tab and in the 95 | first browser tab. 96 | 97 | Now your chat server is fully asynchronous! 98 | 99 | This tutorial continues in :doc:`Tutorial 4 `. 100 | -------------------------------------------------------------------------------- /channels/testing/websocket.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import unquote, urlparse 3 | 4 | from channels.testing.application import ApplicationCommunicator 5 | 6 | 7 | class WebsocketCommunicator(ApplicationCommunicator): 8 | """ 9 | ApplicationCommunicator subclass that has WebSocket shortcut methods. 10 | 11 | It will construct the scope for you, so you need to pass the application 12 | (uninstantiated) along with the initial connection parameters. 13 | """ 14 | 15 | def __init__( 16 | self, application, path, headers=None, subprotocols=None, spec_version=None 17 | ): 18 | if not isinstance(path, str): 19 | raise TypeError("Expected str, got {}".format(type(path))) 20 | parsed = urlparse(path) 21 | self.scope = { 22 | "type": "websocket", 23 | "path": unquote(parsed.path), 24 | "query_string": parsed.query.encode("utf-8"), 25 | "headers": headers or [], 26 | "subprotocols": subprotocols or [], 27 | } 28 | if spec_version: 29 | self.scope["spec_version"] = spec_version 30 | super().__init__(application, self.scope) 31 | self.response_headers = None 32 | 33 | async def connect(self, timeout=1): 34 | """ 35 | Trigger the connection code. 36 | 37 | On an accepted connection, returns (True, ) 38 | On a rejected connection, returns (False, ) 39 | """ 40 | await self.send_input({"type": "websocket.connect"}) 41 | response = await self.receive_output(timeout) 42 | if response["type"] == "websocket.close": 43 | return (False, response.get("code", 1000)) 44 | else: 45 | assert response["type"] == "websocket.accept" 46 | self.response_headers = response.get("headers", []) 47 | return (True, response.get("subprotocol", None)) 48 | 49 | async def send_to(self, text_data=None, bytes_data=None): 50 | """ 51 | Sends a WebSocket frame to the application. 52 | """ 53 | # Make sure we have exactly one of the arguments 54 | assert bool(text_data) != bool( 55 | bytes_data 56 | ), "You must supply exactly one of text_data or bytes_data" 57 | # Send the right kind of event 58 | if text_data: 59 | assert isinstance(text_data, str), "The text_data argument must be a str" 60 | await self.send_input({"type": "websocket.receive", "text": text_data}) 61 | else: 62 | assert isinstance( 63 | bytes_data, bytes 64 | ), "The bytes_data argument must be bytes" 65 | await self.send_input({"type": "websocket.receive", "bytes": bytes_data}) 66 | 67 | async def send_json_to(self, data): 68 | """ 69 | Sends JSON data as a text frame 70 | """ 71 | await self.send_to(text_data=json.dumps(data)) 72 | 73 | async def receive_from(self, timeout=1): 74 | """ 75 | Receives a data frame from the view. Will fail if the connection 76 | closes instead. Returns either a bytestring or a unicode string 77 | depending on what sort of frame you got. 78 | """ 79 | response = await self.receive_output(timeout) 80 | # Make sure this is a send message 81 | assert ( 82 | response["type"] == "websocket.send" 83 | ), f"Expected type 'websocket.send', but was '{response['type']}'" 84 | # Make sure there's exactly one key in the response 85 | assert ("text" in response) != ( 86 | "bytes" in response 87 | ), "The response needs exactly one of 'text' or 'bytes'" 88 | # Pull out the right key and typecheck it for our users 89 | if "text" in response: 90 | assert isinstance( 91 | response["text"], str 92 | ), f"Text frame payload is not str, it is {type(response['text'])}" 93 | return response["text"] 94 | else: 95 | assert isinstance( 96 | response["bytes"], bytes 97 | ), f"Binary frame payload is not bytes, it is {type(response['bytes'])}" 98 | return response["bytes"] 99 | 100 | async def receive_json_from(self, timeout=1): 101 | """ 102 | Receives a JSON text frame payload and decodes it 103 | """ 104 | payload = await self.receive_from(timeout) 105 | assert isinstance( 106 | payload, str 107 | ), f"JSON data is not a text frame, it is {type(payload)}" 108 | return json.loads(payload) 109 | 110 | async def disconnect(self, code=1000, timeout=1): 111 | """ 112 | Closes the socket 113 | """ 114 | await self.send_input({"type": "websocket.disconnect", "code": code}) 115 | await self.wait(timeout) 116 | -------------------------------------------------------------------------------- /channels/consumer.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from asgiref.sync import async_to_sync 4 | 5 | from . import DEFAULT_CHANNEL_LAYER 6 | from .db import aclose_old_connections, database_sync_to_async 7 | from .exceptions import StopConsumer 8 | from .layers import get_channel_layer 9 | from .utils import await_many_dispatch 10 | 11 | 12 | def get_handler_name(message): 13 | """ 14 | Looks at a message, checks it has a sensible type, and returns the 15 | handler name for that type. 16 | """ 17 | # Check message looks OK 18 | if "type" not in message: 19 | raise ValueError("Incoming message has no 'type' attribute") 20 | # Extract type and replace . with _ 21 | handler_name = message["type"].replace(".", "_") 22 | if handler_name.startswith("_"): 23 | raise ValueError("Malformed type in message (leading underscore)") 24 | return handler_name 25 | 26 | 27 | class AsyncConsumer: 28 | """ 29 | Base consumer class. Implements the ASGI application spec, and adds on 30 | channel layer management and routing of events to named methods based 31 | on their type. 32 | """ 33 | 34 | _sync = False 35 | channel_layer_alias = DEFAULT_CHANNEL_LAYER 36 | 37 | async def __call__(self, scope, receive, send): 38 | """ 39 | Dispatches incoming messages to type-based handlers asynchronously. 40 | """ 41 | self.scope = scope 42 | 43 | # Initialize channel layer 44 | self.channel_layer = get_channel_layer(self.channel_layer_alias) 45 | if self.channel_layer is not None: 46 | self.channel_name = await self.channel_layer.new_channel() 47 | self.channel_receive = functools.partial( 48 | self.channel_layer.receive, self.channel_name 49 | ) 50 | # Store send function 51 | if self._sync: 52 | self.base_send = async_to_sync(send) 53 | else: 54 | self.base_send = send 55 | # Pass messages in from channel layer or client to dispatch method 56 | try: 57 | if self.channel_layer is not None: 58 | await await_many_dispatch( 59 | [receive, self.channel_receive], self.dispatch 60 | ) 61 | else: 62 | await await_many_dispatch([receive], self.dispatch) 63 | except StopConsumer: 64 | # Exit cleanly 65 | pass 66 | 67 | async def dispatch(self, message): 68 | """ 69 | Works out what to do with a message. 70 | """ 71 | handler = getattr(self, get_handler_name(message), None) 72 | if handler: 73 | await aclose_old_connections() 74 | await handler(message) 75 | else: 76 | raise ValueError("No handler for message type %s" % message["type"]) 77 | 78 | async def send(self, message): 79 | """ 80 | Overrideable/callable-by-subclasses send method. 81 | """ 82 | await self.base_send(message) 83 | 84 | @classmethod 85 | def as_asgi(cls, **initkwargs): 86 | """ 87 | Return an ASGI v3 single callable that instantiates a consumer instance 88 | per scope. Similar in purpose to Django's as_view(). 89 | 90 | initkwargs will be used to instantiate the consumer instance. 91 | """ 92 | 93 | async def app(scope, receive, send): 94 | consumer = cls(**initkwargs) 95 | return await consumer(scope, receive, send) 96 | 97 | app.consumer_class = cls 98 | app.consumer_initkwargs = initkwargs 99 | 100 | # take name and docstring from class 101 | functools.update_wrapper(app, cls, updated=()) 102 | return app 103 | 104 | 105 | class SyncConsumer(AsyncConsumer): 106 | """ 107 | Synchronous version of the consumer, which is what we write most of the 108 | generic consumers against (for now). Calls handlers in a threadpool and 109 | uses CallBouncer to get the send method out to the main event loop. 110 | 111 | It would have been possible to have "mixed" consumers and auto-detect 112 | if a handler was awaitable or not, but that would have made the API 113 | for user-called methods very confusing as there'd be two types of each. 114 | """ 115 | 116 | _sync = True 117 | 118 | @database_sync_to_async 119 | def dispatch(self, message): 120 | """ 121 | Dispatches incoming messages to type-based handlers asynchronously. 122 | """ 123 | # Get and execute the handler 124 | handler = getattr(self, get_handler_name(message), None) 125 | if handler: 126 | handler(message) 127 | else: 128 | raise ValueError("No handler for message type %s" % message["type"]) 129 | 130 | def send(self, message): 131 | """ 132 | Overrideable/callable-by-subclasses send method. 133 | """ 134 | self.base_send(message) 135 | -------------------------------------------------------------------------------- /docs/topics/routing.rst: -------------------------------------------------------------------------------- 1 | Routing 2 | ======= 3 | 4 | While consumers are valid :doc:`ASGI ` applications, you don't want 5 | to just write one and have that be the only thing you can give to protocol 6 | servers like Daphne. Channels provides routing classes that allow you to 7 | combine and stack your consumers (and any other valid ASGI application) to 8 | dispatch based on what the connection is. 9 | 10 | .. important:: 11 | 12 | Channels routers only work on the *scope* level, not on the level of 13 | individual *events*, which means you can only have one consumer for any 14 | given connection. Routing is to work out what single consumer to give a 15 | connection, not how to spread events from one connection across 16 | multiple consumers. 17 | 18 | Routers are themselves valid ASGI applications, and it's possible to nest them. 19 | We suggest that you have a ``ProtocolTypeRouter`` as the root application of 20 | your project - the one that you pass to protocol servers - and nest other, 21 | more protocol-specific routing underneath there. 22 | 23 | Channels expects you to be able to define a single *root application*, and 24 | provide the path to it as the ``ASGI_APPLICATION`` setting (think of this as 25 | being analogous to the ``ROOT_URLCONF`` setting in Django). There's no fixed 26 | rule as to where you need to put the routing and the root application, but we 27 | recommend following Django's conventions and putting them in a project-level 28 | file called ``asgi.py``, next to ``urls.py``. You can read more about deploying 29 | Channels projects and settings in :doc:`/deploying`. 30 | 31 | Here's an example of what that ``asgi.py`` might look like: 32 | 33 | .. include:: ../includes/asgi_example.rst 34 | 35 | 36 | .. note:: 37 | We call the ``as_asgi()`` classmethod when routing our consumers. This 38 | returns an ASGI wrapper application that will instantiate a new consumer 39 | instance for each connection or scope. This is similar to Django's 40 | ``as_view()``, which plays the same role for per-request instances of 41 | class-based views. 42 | 43 | It's possible to have routers from third-party apps, too, or write your own, 44 | but we'll go over the built-in Channels ones here. 45 | 46 | 47 | ProtocolTypeRouter 48 | ------------------ 49 | 50 | ``channels.routing.ProtocolTypeRouter`` 51 | 52 | This should be the top level of your ASGI application stack and the main entry 53 | in your routing file. 54 | 55 | It lets you dispatch to one of a number of other ASGI applications based on the 56 | ``type`` value present in the ``scope``. Protocols will define a fixed type 57 | value that their scope contains, so you can use this to distinguish between 58 | incoming connection types. 59 | 60 | It takes a single argument - a dictionary mapping type names to ASGI 61 | applications that serve them: 62 | 63 | .. code-block:: python 64 | 65 | ProtocolTypeRouter({ 66 | "http": some_app, 67 | "websocket": some_other_app, 68 | }) 69 | 70 | If you want to split HTTP handling between long-poll handlers and Django views, 71 | use a URLRouter using Django's ``get_asgi_application()`` specified as the last 72 | entry with a match-everything pattern. 73 | 74 | .. _urlrouter: 75 | 76 | URLRouter 77 | --------- 78 | 79 | ``channels.routing.URLRouter`` 80 | 81 | Routes ``http`` or ``websocket`` type connections via their HTTP path. Takes a 82 | single argument, a list of Django URL objects (either ``path()`` or 83 | ``re_path()``): 84 | 85 | .. code-block:: python 86 | 87 | URLRouter([ 88 | re_path(r"^longpoll/$", LongPollConsumer.as_asgi()), 89 | re_path(r"^notifications/(?P\w+)/$", LongPollConsumer.as_asgi()), 90 | re_path(r"", get_asgi_application()), 91 | ]) 92 | 93 | Any captured groups will be provided in ``scope`` as the key ``url_route``, a 94 | dict with a ``kwargs`` key containing a dict of the named regex groups and 95 | an ``args`` key with a list of positional regex groups. Note that named 96 | and unnamed groups cannot be mixed: Positional groups are discarded as soon 97 | as a single named group is matched. 98 | 99 | For example, to pull out the named group ``stream`` in the example above, you 100 | would do this: 101 | 102 | .. code-block:: python 103 | 104 | stream = self.scope["url_route"]["kwargs"]["stream"] 105 | 106 | Please note that ``URLRouter`` nesting will not work properly with 107 | ``path()`` routes if inner routers are wrapped by additional middleware. 108 | See `Issue #1428 `__. 109 | 110 | 111 | ChannelNameRouter 112 | ----------------- 113 | 114 | ``channels.routing.ChannelNameRouter`` 115 | 116 | Routes ``channel`` type scopes based on the value of the ``channel`` key in 117 | their scope. Intended for use with the :doc:`/topics/worker`. 118 | 119 | It takes a single argument - a dictionary mapping channel names to ASGI 120 | applications that serve them: 121 | 122 | .. code-block:: python 123 | 124 | ChannelNameRouter({ 125 | "thumbnails-generate": some_app, 126 | "thumbnails-delete": some_other_app, 127 | }) 128 | -------------------------------------------------------------------------------- /loadtesting/2016-09-06/README.rst: -------------------------------------------------------------------------------- 1 | Django Channels Load Testing Results for (2016-09-06) 2 | ===================================================== 3 | 4 | The goal of these load tests is to see how Channels performs with normal HTTP traffic under heavy load. 5 | 6 | In order to handle WebSockets, Channels introduced ASGI, a new interface spec for asynchronous request handling. Also, 7 | Channels implemented this spec with Daphne--an HTTP, HTTP2, and WebSocket protocol server. 8 | 9 | The load testing completed has been to compare how well Daphne using 1 worker performs with normal HTTP traffic in 10 | comparison to a WSGI HTTP server. Gunicorn was chosen as its configuration was simple and well-understood. 11 | 12 | 13 | Summary of Results 14 | ~~~~~~~~~~~~~~~~~~ 15 | 16 | Daphne is not as efficient as its WSGI counterpart. When considering only latency, Daphne can have 10 times the latency 17 | when under the same traffic load as gunicorn. When considering only throughput, Daphne can have 40-50% of the total 18 | throughput of gunicorn while still being at 2 times latency. 19 | 20 | The results should not be surprising considering the overhead involved. However, these results represent the simplest 21 | case to test and should be represented as saying that Daphne is always slower than an WSGI server. These results are 22 | a starting point, not a final conclusion. 23 | 24 | Some additional things that should be tested: 25 | 26 | - More than 1 worker 27 | - A separate server for redis 28 | - Comparison to other WebSocket servers, such as Node's socket.io or Rails' Action cable 29 | 30 | 31 | Methodology 32 | ~~~~~~~~~~~ 33 | 34 | In order to control for variances, several measures were taken: 35 | 36 | - the same testing tool was used across all tests, `loadtest `_. 37 | - all target machines were identical 38 | - all target code variances were separated into appropriate files in the dir of /testproject in this repo 39 | - all target config variances necessary to the different setups were controlled by supervisord so that human error was limited 40 | - across different test types, the same target machines were used, using the same target code and the same target config 41 | - several tests were run for each setup and test type 42 | 43 | 44 | Setups 45 | ~~~~~~ 46 | 47 | 3 setups were used for this set of tests: 48 | 49 | 1) Normal Django with Gunicorn (19.6.0) 50 | 2) Django Channels with local Redis (0.14.0) and Daphne (0.14.3) 51 | 3) Django Channels with IPC (1.1.0) and Daphne (0.14.3) 52 | 53 | 54 | Latency 55 | ~~~~~~~ 56 | 57 | All target and sources machines were identical ec2 instances m3.2xlarge running Ubuntu 16.04. 58 | 59 | In order to ensure that the same number of requests were sent, the rps flag was set to 300. 60 | 61 | 62 | .. image:: channels-latency.PNG 63 | 64 | 65 | Throughput 66 | ~~~~~~~~~~ 67 | 68 | The same source machine was used for all tests: ec2 instance m3.large running Ubuntu 16.04. 69 | All target machines were identical ec2 instances m3.2xlarge running Ubuntu 16.04. 70 | 71 | For the following tests, loadtest was permitted to autothrottle so as to limit errors; this led to varied latency times. 72 | 73 | Gunicorn had a latency of 6 ms; daphne and Redis, 12 ms; daphne and IPC, 35 ms. 74 | 75 | 76 | .. image:: channels-throughput.PNG 77 | 78 | 79 | Supervisor Configs 80 | ~~~~~~~~~~~~~~~~~~ 81 | 82 | **Gunicorn (19.6.0)** 83 | 84 | This is the non-channels config. It's a standard Django environment on one machine, using gunicorn to handle requests. 85 | 86 | .. code-block:: bash 87 | 88 | [program:gunicorn] 89 | command = gunicorn testproject.wsgi_no_channels -b 0.0.0.0:80 90 | directory = /srv/channels/testproject/ 91 | user = root 92 | 93 | [group:django_http] 94 | programs=gunicorn 95 | priority=999 96 | 97 | 98 | **Redis (0.14.0) and Daphne (0.14.3)** 99 | 100 | This is the channels config using redis as the backend. It's on one machine, so a local redis config. 101 | 102 | Also, it's a single worker, not multiple, as that's the default config. 103 | 104 | .. code-block:: bash 105 | 106 | [program:daphne] 107 | command = daphne -b 0.0.0.0 -p 80 testproject.asgi:channel_layer 108 | directory = /srv/channels/testproject/ 109 | user = root 110 | 111 | [program:worker] 112 | command = python manage.py runworker 113 | directory = /srv/channels/testproject/ 114 | user = django-channels 115 | 116 | 117 | [group:django_channels] 118 | programs=daphne,worker 119 | priority=999 120 | 121 | 122 | **IPC (1.1.0) and Daphne (0.14.3)** 123 | 124 | This is the channels config using IPC (Inter Process Communication). It's only possible to have this work on one machine. 125 | 126 | 127 | .. code-block:: bash 128 | 129 | [program:daphne] 130 | command = daphne -b 0.0.0.0 -p 80 testproject.asgi_for_ipc:channel_layer 131 | directory = /srv/channels/testproject/ 132 | user = root 133 | 134 | [program:worker] 135 | command = python manage.py runworker --settings=testproject.settings.channels_ipc 136 | directory = /srv/channels/testproject/ 137 | user = root 138 | 139 | 140 | [group:django_channels] 141 | programs=daphne,worker 142 | priority=999 143 | -------------------------------------------------------------------------------- /docs/releases/2.1.0.rst: -------------------------------------------------------------------------------- 1 | 2.1.0 Release Notes 2 | =================== 3 | 4 | Channels 2.1 brings a few new major changes to Channels as well as some more 5 | minor fixes. In addition, if you've not yet seen it, we now have a long-form 6 | :doc:`tutorial ` to better introduce some of the concepts 7 | and sync versus async styles of coding. 8 | 9 | 10 | Major Changes 11 | ------------- 12 | 13 | Async HTTP Consumer 14 | ~~~~~~~~~~~~~~~~~~~ 15 | 16 | There is a new native-async HTTP consumer class, 17 | ``channels.generic.http.AsyncHttpConsumer``. This allows much easier writing 18 | of long-poll endpoints or other long-lived HTTP connection handling that 19 | benefits from native async support. 20 | 21 | You can read more about it in the :doc:`/topics/consumers` documentation. 22 | 23 | 24 | WebSocket Consumers 25 | ~~~~~~~~~~~~~~~~~~~ 26 | 27 | These consumer classes now all have built-in group join and leave functionality, 28 | which will make a consumer join all group names that are in the iterable 29 | ``groups`` on the consumer class (this can be a static list or a ``@property`` 30 | method). 31 | 32 | In addition, the ``accept`` methods on both variants now take an optional 33 | ``subprotocol`` argument, which will be sent back to the WebSocket client as 34 | the subprotocol the server has selected. The client's advertised subprotocols 35 | can, as always, be found in the scope as ``scope["subprotocols"]``. 36 | 37 | 38 | Nested URL Routing 39 | ~~~~~~~~~~~~~~~~~~ 40 | 41 | ``URLRouter`` instances can now be nested inside each other and, like Django's 42 | URL handling and ``include``, will strip off the matched part of the URL in the 43 | outer router and leave only the unmatched portion for the inner router, allowing 44 | reusable routing files. 45 | 46 | Note that you **cannot** use the Django ``include`` function inside of the 47 | ``URLRouter`` as it assumes a bit too much about what it is given as its 48 | left-hand side and will terminate your regular expression/URL pattern wrongly. 49 | 50 | 51 | Login and Logout 52 | ~~~~~~~~~~~~~~~~ 53 | 54 | As well as overhauling the internals of the ``AuthMiddleware``, there are now 55 | also ``login`` and ``logout`` async functions you can call in consumers to 56 | log users in and out of the current session. 57 | 58 | Due to the way cookies are sent back to clients, these come with some caveats; 59 | read more about them and how to use them properly in :doc:`/topics/authentication`. 60 | 61 | 62 | In-Memory Channel Layer 63 | ~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | The in-memory channel layer has been extended to have full expiry and group 66 | support so it should now be suitable for drop-in replacement for most 67 | test scenarios. 68 | 69 | 70 | Testing 71 | ~~~~~~~ 72 | 73 | The ``ChannelsLiveServerTestCase`` has been rewritten to use a new method for 74 | launching Daphne that should be more resilient (and faster), and now shares 75 | code with the Daphne test suite itself. 76 | 77 | Ports are now left up to the operating 78 | system to decide rather than being picked from within a set range. It also now 79 | supports static files when the Django ``staticfiles`` app is enabled. 80 | 81 | In addition, the Communicator classes have gained a ``receive_nothing`` method 82 | that allows you to assert that the application didn't send anything, rather 83 | than writing this yourself using exception handling. See more in the 84 | :doc:`/topics/testing` documentation. 85 | 86 | 87 | Origin header validation 88 | ~~~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | As well as removing the ``print`` statements that accidentally got into the 91 | last release, this has been overhauled to more correctly match against headers 92 | according to the Origin header spec and align with Django's ``ALLOWED_HOSTS`` 93 | setting. 94 | 95 | It can now also enforce protocol (``http`` versus ``https``) and port, both 96 | optionally. 97 | 98 | 99 | Bugfixes & Small Changes 100 | ------------------------ 101 | 102 | * ``print`` statements that accidentally got left in the ``Origin`` validation 103 | code were removed. 104 | 105 | * The ``runserver`` command now shows the version of Channels you are running. 106 | 107 | * Orphaned tasks that may have caused warnings during test runs or occasionally 108 | live site traffic are now correctly killed off rather than letting them die 109 | later on and print warning messages. 110 | 111 | * ``WebsocketCommunicator`` now accepts a query string passed into the 112 | constructor and adds it to the scope rather than just ignoring it. 113 | 114 | * Test handlers will correctly handle changing the ``CHANNEL_LAYERS`` setting 115 | via decorators and wipe the internal channel layer cache. 116 | 117 | * ``SessionMiddleware`` can be safely nested inside itself rather than causing 118 | a runtime error. 119 | 120 | 121 | Backwards Incompatible Changes 122 | ------------------------------ 123 | 124 | * The format taken by the ``OriginValidator`` for its domains has changed and 125 | ``*.example.com`` is no longer allowed; instead, use ``.example.com`` to match 126 | a domain and all its subdomains. 127 | 128 | * If you previously nested ``URLRouter`` instances inside each other both would 129 | have been matching on the full URL before, whereas now they will match on the 130 | unmatched portion of the URL, meaning your URL routes would break if you had 131 | intended this usage. 132 | -------------------------------------------------------------------------------- /tests/test_generic_http.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import time 4 | 5 | import pytest 6 | 7 | from channels.generic.http import AsyncHttpConsumer 8 | from channels.testing import HttpCommunicator 9 | 10 | 11 | @pytest.mark.django_db(transaction=True) 12 | @pytest.mark.asyncio 13 | async def test_async_http_consumer(): 14 | """ 15 | Tests that AsyncHttpConsumer is implemented correctly. 16 | """ 17 | 18 | class TestConsumer(AsyncHttpConsumer): 19 | async def handle(self, body): 20 | data = json.loads(body.decode("utf-8")) 21 | await self.send_response( 22 | 200, 23 | json.dumps({"value": data["value"]}).encode("utf-8"), 24 | headers={b"Content-Type": b"application/json"}, 25 | ) 26 | 27 | app = TestConsumer() 28 | 29 | # Open a connection 30 | communicator = HttpCommunicator( 31 | app, 32 | method="POST", 33 | path="/test/", 34 | body=json.dumps({"value": 42, "anything": False}).encode("utf-8"), 35 | ) 36 | response = await communicator.get_response() 37 | assert response["body"] == b'{"value": 42}' 38 | assert response["status"] == 200 39 | assert response["headers"] == [(b"Content-Type", b"application/json")] 40 | 41 | 42 | @pytest.mark.django_db(transaction=True) 43 | @pytest.mark.asyncio 44 | async def test_error(): 45 | class TestConsumer(AsyncHttpConsumer): 46 | async def handle(self, body): 47 | raise AssertionError("Error correctly raised") 48 | 49 | communicator = HttpCommunicator(TestConsumer(), "GET", "/") 50 | with pytest.raises(AssertionError) as excinfo: 51 | await communicator.get_response(timeout=0.05) 52 | 53 | assert str(excinfo.value) == "Error correctly raised" 54 | 55 | 56 | @pytest.mark.django_db(transaction=True) 57 | @pytest.mark.asyncio 58 | async def test_per_scope_consumers(): 59 | """ 60 | Tests that a distinct consumer is used per scope, with AsyncHttpConsumer as 61 | the example consumer class. 62 | """ 63 | 64 | class TestConsumer(AsyncHttpConsumer): 65 | def __init__(self): 66 | super().__init__() 67 | self.time = time.time() 68 | 69 | async def handle(self, body): 70 | body = f"{self.__class__.__name__} {id(self)} {self.time}" 71 | 72 | await self.send_response( 73 | 200, 74 | body.encode("utf-8"), 75 | headers={b"Content-Type": b"text/plain"}, 76 | ) 77 | 78 | app = TestConsumer.as_asgi() 79 | 80 | # Open a connection 81 | communicator = HttpCommunicator(app, method="GET", path="/test/") 82 | response = await communicator.get_response() 83 | assert response["status"] == 200 84 | 85 | # And another one. 86 | communicator = HttpCommunicator(app, method="GET", path="/test2/") 87 | second_response = await communicator.get_response() 88 | assert second_response["status"] == 200 89 | 90 | assert response["body"] != second_response["body"] 91 | 92 | 93 | @pytest.mark.django_db(transaction=True) 94 | @pytest.mark.asyncio 95 | async def test_async_http_consumer_future(): 96 | """ 97 | Regression test for channels accepting only coroutines. The ASGI specification 98 | states that the `receive` and `send` arguments to an ASGI application should be 99 | "awaitable callable" objects. That includes non-coroutine functions that return 100 | Futures. 101 | """ 102 | 103 | class TestConsumer(AsyncHttpConsumer): 104 | async def handle(self, body): 105 | await self.send_response( 106 | 200, 107 | b"42", 108 | headers={b"Content-Type": b"text/plain"}, 109 | ) 110 | 111 | app = TestConsumer() 112 | 113 | # Ensure the passed functions are specifically coroutines. 114 | async def coroutine_app(scope, receive, send): 115 | async def receive_coroutine(): 116 | return await asyncio.ensure_future(receive()) 117 | 118 | async def send_coroutine(*args, **kwargs): 119 | return await asyncio.ensure_future(send(*args, **kwargs)) 120 | 121 | await app(scope, receive_coroutine, send_coroutine) 122 | 123 | communicator = HttpCommunicator(coroutine_app, method="GET", path="/") 124 | response = await communicator.get_response() 125 | assert response["body"] == b"42" 126 | assert response["status"] == 200 127 | assert response["headers"] == [(b"Content-Type", b"text/plain")] 128 | 129 | # Ensure the passed functions are "Awaitable Callables" and NOT coroutines. 130 | async def awaitable_callable_app(scope, receive, send): 131 | def receive_awaitable_callable(): 132 | return asyncio.ensure_future(receive()) 133 | 134 | def send_awaitable_callable(*args, **kwargs): 135 | return asyncio.ensure_future(send(*args, **kwargs)) 136 | 137 | await app(scope, receive_awaitable_callable, send_awaitable_callable) 138 | 139 | # Open a connection 140 | communicator = HttpCommunicator(awaitable_callable_app, method="GET", path="/") 141 | response = await communicator.get_response() 142 | assert response["body"] == b"42" 143 | assert response["status"] == 200 144 | assert response["headers"] == [(b"Content-Type", b"text/plain")] 145 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you're looking to contribute to Channels, then please read on - we encourage 5 | contributions both large and small, from both novice and seasoned developers. 6 | 7 | 8 | What can I work on? 9 | ------------------- 10 | 11 | We're looking for help with the following areas: 12 | 13 | * Documentation and tutorial writing 14 | * Bugfixing and testing 15 | * Feature polish and occasional new feature design 16 | * Case studies and writeups 17 | 18 | You can find what we're looking to work on in the GitHub issues list for each 19 | of the Channels sub-projects: 20 | 21 | * `Channels issues `_, for the Django integration and overall project efforts 22 | * `Daphne issues `_, for the HTTP and Websocket termination 23 | * `asgiref issues `_, for the base ASGI library/memory backend 24 | * `channels_redis issues `_, for the Redis channel backend 25 | 26 | Issues are categorized by difficulty level: 27 | 28 | * ``exp/beginner``: Easy issues suitable for a first-time contributor. 29 | * ``exp/intermediate``: Moderate issues that need skill and a day or two to solve. 30 | * ``exp/advanced``: Difficult issues that require expertise and potentially weeks of work. 31 | 32 | They are also classified by type: 33 | 34 | * ``documentation``: Documentation issues. Pick these if you want to help us by writing docs. 35 | * ``bug``: A bug in existing code. Usually easier for beginners as there's a defined thing to fix. 36 | * ``enhancement``: A new feature for the code; may be a bit more open-ended. 37 | 38 | You should filter the issues list by the experience level and type of work 39 | you'd like to do, and then if you want to take something on leave a comment 40 | and assign yourself to it. If you want advice about how to take on a bug, 41 | leave a comment asking about it and we'll be happy to help. 42 | 43 | The issues are also just a suggested list - any offer to help is welcome as 44 | long as it fits the project goals, but you should make an issue for the thing 45 | you wish to do and discuss it first if it's relatively large (but if you just 46 | found a small bug and want to fix it, sending us a pull request straight away 47 | is fine). 48 | 49 | 50 | I'm a novice contributor/developer - can I help? 51 | ------------------------------------------------ 52 | 53 | Of course! The issues labelled with ``exp/beginner`` are a perfect place to get 54 | started, as they're usually small and well defined. If you want help with one 55 | of them, jump in and comment on the ticket if you need input or assistance. 56 | 57 | 58 | How do I get started and run the tests? 59 | --------------------------------------- 60 | 61 | First, you should first clone the git repository to a local directory: 62 | 63 | .. code-block:: sh 64 | 65 | git clone https://github.com/django/channels.git channels 66 | 67 | Next, you may want to make a virtual environment to run the tests and develop 68 | in; you can use either ``virtualenvwrapper``, ``pipenv`` or just plain 69 | ``virtualenv`` for this. 70 | 71 | Then, ``cd`` into the ``channels`` directory and install it editable into 72 | your environment: 73 | 74 | .. code-block:: sh 75 | 76 | cd channels/ 77 | python -m pip install -e .[tests] 78 | 79 | Note the ``[tests]`` section there; that tells ``pip`` that you want to install 80 | the ``tests`` extra, which will bring in testing dependencies like 81 | ``pytest-django``. 82 | 83 | Then, you can run the tests: 84 | 85 | .. code-block:: sh 86 | 87 | pytest 88 | 89 | Also, there is a tox.ini file at the root of the repository. Example commands: 90 | 91 | .. code-block:: sh 92 | 93 | $ tox -l 94 | py37-dj32 95 | py38-dj32 96 | py39-dj32 97 | py310-dj32 98 | py38-dj40 99 | py38-dj41 100 | py38-djmain 101 | py39-dj40 102 | py39-dj41 103 | py39-djmain 104 | py310-dj40 105 | py310-dj41 106 | py310-djmain 107 | qa 108 | 109 | # run the test with Python 3.10, on Django 4.1 and Django main branch 110 | $ tox -e py310-dj41,py310-djmain 111 | 112 | Note that tox can also forward arguments to pytest. When using pdb with pytest, 113 | forward the ``-s`` option to pytest as such: 114 | 115 | .. code-block:: sh 116 | 117 | tox -e py310-dj41 -- -s 118 | 119 | The ``qa`` environment runs the various linters used by the project. 120 | 121 | How do I do a release? 122 | ---------------------- 123 | 124 | If you have commit access, a release involves the following steps: 125 | 126 | * Create a new entry in the CHANGELOG.txt file and summarise the changes 127 | * Create a new release page in the docs under ``docs/releases`` and add the 128 | changelog there with more information where necessary 129 | * Add a link to the new release notes in ``docs/releases/index.rst`` 130 | * Set the new version in ``__init__.py`` 131 | * Roll all of these up into a single commit and tag it with the new version 132 | number. Push the commit and tag. 133 | * To upload you will need to be added as a maintainer on PyPI. 134 | Run `python setup.py sdist bdist_wheel`, and `twine upload`. 135 | 136 | The release process for ``channels-redis`` and ``daphne`` is similar, but 137 | they don't have the two steps in ``docs/``. 138 | -------------------------------------------------------------------------------- /docs/support.rst: -------------------------------------------------------------------------------- 1 | Support 2 | ======= 3 | 4 | If you have questions about Channels, need debugging help or technical support, you can turn to community resources like: 5 | 6 | - `Stack Overflow `_ 7 | - The `Django Users mailing list `_ (django-users@googlegroups.com) 8 | - The #django channel on the `PySlackers Slack group `_ 9 | 10 | If you have a concrete bug or feature request (one that is clear and actionable), please file an issue against the 11 | appropriate GitHub project. 12 | 13 | Unfortunately, if you open a GitHub issue with a vague problem (like "it's slow!" or "connections randomly drop!") 14 | we'll have to close it as we don't have the volunteers to answer the number of questions we'd get - please go to 15 | one of the other places above for support from the community at large. 16 | 17 | As a guideline, your issue is concrete enough to open an issue if you can provide **exact steps to reproduce** in a fresh, 18 | example project. We need to be able to reproduce it on a *normal, local developer machine* - so saying something doesn't 19 | work in a hosted environment is unfortunately not very useful to us, and we'll close the issue and point you here. 20 | 21 | Apologies if this comes off as harsh, but please understand that open source maintenance and support takes up a lot 22 | of time, and if we answered all the issues and support requests there would be no time left to actually work on the code 23 | itself! 24 | 25 | Making bugs reproducible 26 | ------------------------ 27 | 28 | If you're struggling with an issue that only happens in a production environment and can't get it to reproduce locally 29 | so either you can fix it or someone can help you, take a step-by-step approach to eliminating the differences between the 30 | environments. 31 | 32 | First off, try changing your production environment to see if that helps - for example, if you have Nginx/Apache/etc. 33 | between browsers and Channels, try going direct to the Python server and see if that fixes things. Turn SSL off if you 34 | have it on. Try from different browsers and internet connections. WebSockets are notoriously hard to debug already, 35 | and so you should expect some level of awkwardness from any project involving them. 36 | 37 | Next, check package versions between your local and remote environments. You'd be surprised how easy it is to forget 38 | to upgrade something! 39 | 40 | Once you've made sure it's none of that, try changing your project. Make a fresh Django project (or use one of the 41 | Channels example projects) and make sure it doesn't have the bug, then work on adding code to it from your project 42 | until the bug appears. Alternately, take your project and remove pieces back down to the basic Django level until 43 | it works. 44 | 45 | Network programming is also just difficult in general; you should expect some level of reconnects and dropped connections 46 | as a matter of course. Make sure that what you're seeing isn't just normal for a production application. 47 | 48 | How to help the Channels project 49 | -------------------------------- 50 | 51 | If you'd like to help us with support, the first thing to do is to provide support in the communities mentioned at the 52 | top (Stack Overflow and the mailing list). 53 | 54 | If you'd also like to help triage issues, please get in touch and mention you'd like to help out and we can make sure you're 55 | set up and have a good idea of what to do. Most of the work is making sure incoming issues are actually valid and actionable, 56 | and closing those that aren't and redirecting them to this page politely and explaining why. 57 | 58 | Some sample response templates are below. 59 | 60 | General support request 61 | ~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | .. code-block:: text 64 | 65 | Sorry, but we can't help out with general support requests here - the issue tracker is for reproducible bugs and 66 | concrete feature requests only! Please see our support documentation (https://channels.readthedocs.io/en/latest/support.html) 67 | for more information about where you can get general help. 68 | 69 | Non-specific bug/"It doesn't work!" 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | .. code-block:: text 73 | 74 | I'm afraid we can't address issues without either direct steps to reproduce, or that only happen in a production 75 | environment, as they may not be problems in the project itself. Our support documentation 76 | (https://channels.readthedocs.io/en/latest/support.html) has details about how to take this sort of problem, diagnose it, 77 | and either fix it yourself, get help from the community, or make it into an actionable issue that we can handle. 78 | 79 | Sorry we have to direct you away like this, but we get a lot of support requests every week. If you can reduce the problem 80 | to a clear set of steps to reproduce or an example project that fails in a fresh environment, please re-open the ticket 81 | with that information. 82 | 83 | Problem in application code 84 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | 86 | .. code-block:: text 87 | 88 | It looks like a problem in your application code rather than in Channels itself, so I'm going to close the ticket. 89 | If you can trace it down to a problem in Channels itself (with exact steps to reproduce on a fresh or small example 90 | project - see https://channels.readthedocs.io/en/latest/support.html) please re-open the ticket! Thanks. 91 | -------------------------------------------------------------------------------- /docs/releases/4.0.0.rst: -------------------------------------------------------------------------------- 1 | 4.0.0 Release Notes 2 | =================== 3 | 4 | Channels 4 is the next major version of the Channels package. Together with the 5 | matching Daphne v4 and channels-redis v4 releases, it updates dependencies, 6 | fixes issues, and removes outdated code. It so provides the foundation for 7 | Channels development going forward. 8 | 9 | In most cases, you can update now by updating ``channels``, ``daphne``, and 10 | ``channels-redis`` as appropriate, with ``pip``, and by adding ``daphne`` at 11 | the top of your ``INSTALLED_APPS`` setting. 12 | 13 | First ``pip``:: 14 | 15 | pip install -U 'channels[daphne]' channels-redis 16 | 17 | Then in your Django settings file:: 18 | 19 | INSTALLED_APPS = [ 20 | "daphne", 21 | ... 22 | ] 23 | 24 | Read on for the details. 25 | 26 | Updated Python and Django support 27 | --------------------------------- 28 | 29 | In general Channels will try to follow Python and Django supported versions. 30 | 31 | As of release, that means Python 3.7, 3.8, 3.9, and 3.10, as well as Django 32 | 3.2, 4.0, and 4.1 are currently supported. 33 | 34 | As a note, we reserve the right to drop older Python versions, or the older 35 | Django LTS, once the newer one is released, before their official end-of-life 36 | if this is necessary to ease development. 37 | 38 | Dropping older Python and Django versions will be done in minor version 39 | releases, and will not be considered to require a major version change. 40 | 41 | The async support in both Python and Django continues to evolve rapidly. We 42 | advise you to always upgrade to the latest versions in order to avoid issues in 43 | older versions if you're building an async application. 44 | 45 | * Dropped support for Python 3.6. 46 | 47 | * Minimum Django version is now Django 3.2. 48 | 49 | * Added compatibility with Django 4.1. 50 | 51 | Decoupling of the Daphne application server 52 | ------------------------------------------- 53 | 54 | In order to allow users of other ASGI servers to use Channels without the 55 | overhead of Daphne and Twisted, the Daphne application server is now an 56 | optional dependency, installable either directly or with the ``daphne`` extra, 57 | as per the ``pip`` example above. 58 | 59 | * Where Daphne is used ``daphne>=4.0.0`` is required. The ``channels[daphne]`` extra assures this. 60 | 61 | * The ``runserver`` command is moved to the ``daphne`` package. 62 | 63 | In order to use the ``runserver`` command, add ``daphne`` to your 64 | ``INSTALLED_APPS``, before ``django.contrib.staticfiles``:: 65 | 66 | INSTALLED_APPS = [ 67 | "daphne", 68 | ... 69 | ] 70 | 71 | There is a new system check to ensure this ordering. 72 | 73 | Note, the ``runworker`` command remains a part of the ``channels`` app. 74 | 75 | * Use of ``ChannelsLiveServerTestCase`` still requires Daphne. 76 | 77 | Removal of the Django application wrappers 78 | ------------------------------------------ 79 | 80 | In order to add initial ASGI support to Django, Channels originally provided 81 | tools for wrapping your Django application and serving it under ASGI. This 82 | included an ASGI handler class, an ASGI HTTP request object, and an ASGI 83 | compatible version of the staticfiles handler for use with ``runserver`` 84 | 85 | Improved equivalents to all of these are what has been added to Django since 86 | Django version 3.0. As such serving of Django HTTP applications (whether using 87 | sync or async views) under ASGI is now Django's responsibility, and the 88 | matching Channels classes have been removed. 89 | 90 | Use of these classes was deprecated in Channels v3 and, if you've already moved 91 | to the Django equivalents there is nothing further to do. 92 | 93 | * Removed deprecated static files handling in favor of 94 | ``django.contrib.staticfiles``. 95 | 96 | * Removed the deprecated AsgiHandler, which wrapped Django views, in favour of 97 | Django's own ASGI support. You should use Django's ``get_asgi_application`` 98 | to provide the ``http`` handler for ProtocolTypeRouter, or an appropriate 99 | path for URLRouter, in order to route your Django application. 100 | 101 | * The supporting ``AsgiRequest`` is also removed, as it was only used for 102 | ``AsgiHandler``. 103 | 104 | * Removed deprecated automatic routing of ``http`` protocol handler in 105 | ``ProtocolTypeRouter``. You must explicitly register the ``http`` handler in 106 | your application if using ``ProtocolTypeRouter``. 107 | 108 | The minimal ``asgi.py`` file routing the Django ASGI application under a 109 | ``ProtocolTypeRouter`` will now look something like this:: 110 | 111 | import os 112 | 113 | from channels.routing import ProtocolTypeRouter 114 | from django.core.asgi import get_asgi_application 115 | 116 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 117 | 118 | application = ProtocolTypeRouter({ 119 | "http": get_asgi_application(), 120 | }) 121 | 122 | i.e. We use Django's ``get_asgi_application()``, and explicitly route an 123 | ``http`` handler for ``ProtocolTypeRouter``. This is merely for illustration of 124 | the changes. Please see the docs for more complete examples. 125 | 126 | Other changes 127 | ------------- 128 | 129 | * The use of the ``guarantee_single_callable()`` compatibility shim is removed. 130 | All applications must be ASGI v3 single-callables. 131 | 132 | * Removed the ``consumer_started`` and ``consumer_finished`` signals, unused 133 | since the 2.0 rewrite. 134 | 135 | * Fixed ``ChannelsLiveServerTestCase`` when running on systems using the 136 | ``spawn`` multiprocessing start method, such as macOS and Windows. 137 | -------------------------------------------------------------------------------- /tests/test_layers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pytest 4 | from django.test import override_settings 5 | 6 | from channels import DEFAULT_CHANNEL_LAYER 7 | from channels.exceptions import InvalidChannelLayerError 8 | from channels.layers import ( 9 | BaseChannelLayer, 10 | InMemoryChannelLayer, 11 | channel_layers, 12 | get_channel_layer, 13 | ) 14 | 15 | 16 | class TestChannelLayerManager(unittest.TestCase): 17 | @override_settings( 18 | CHANNEL_LAYERS={"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} 19 | ) 20 | def test_config_error(self): 21 | """ 22 | If channel layer doesn't specify TEST_CONFIG, `make_test_backend` 23 | should result into error. 24 | """ 25 | 26 | with self.assertRaises(InvalidChannelLayerError): 27 | channel_layers.make_test_backend(DEFAULT_CHANNEL_LAYER) 28 | 29 | @override_settings( 30 | CHANNEL_LAYERS={ 31 | "default": { 32 | "BACKEND": "channels.layers.InMemoryChannelLayer", 33 | "TEST_CONFIG": {"expiry": 100500}, 34 | } 35 | } 36 | ) 37 | def test_config_instance(self): 38 | """ 39 | If channel layer provides TEST_CONFIG, `make_test_backend` should 40 | return channel layer instance appropriate for testing. 41 | """ 42 | 43 | layer = channel_layers.make_test_backend(DEFAULT_CHANNEL_LAYER) 44 | self.assertEqual(layer.expiry, 100500) 45 | 46 | def test_override_settings(self): 47 | """ 48 | The channel layers cache is reset when the CHANNEL_LAYERS setting 49 | changes. 50 | """ 51 | with override_settings( 52 | CHANNEL_LAYERS={ 53 | "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"} 54 | } 55 | ): 56 | self.assertEqual(channel_layers.backends, {}) 57 | get_channel_layer() 58 | self.assertNotEqual(channel_layers.backends, {}) 59 | self.assertEqual(channel_layers.backends, {}) 60 | 61 | @override_settings( 62 | CHANNEL_LAYERS={ 63 | "default": { 64 | "BACKEND": "tests.test_layers.BrokenBackend", 65 | } 66 | } 67 | ) 68 | def test_backend_import_error_not_hidden(self): 69 | """ 70 | Test that KeyError exceptions within the backend import are not hidden. 71 | This test ensures that the PR #2146 fix works correctly. 72 | """ 73 | # This should raise a KeyError from the backend, not an InvalidChannelLayerError 74 | with self.assertRaises(KeyError): 75 | channel_layers.make_backend(DEFAULT_CHANNEL_LAYER) 76 | 77 | 78 | # Mock backend that raises KeyError during import 79 | class BrokenBackend: 80 | def __init__(self, **kwargs): 81 | # This will be called during backend initialization 82 | # and should raise a KeyError that should not be caught 83 | raise KeyError("This is a deliberate KeyError from the backend") 84 | 85 | 86 | # In-memory layer tests 87 | 88 | 89 | @pytest.mark.asyncio 90 | async def test_send_receive(): 91 | layer = InMemoryChannelLayer() 92 | message = {"type": "test.message"} 93 | await layer.send("test.channel", message) 94 | assert message == await layer.receive("test.channel") 95 | 96 | 97 | @pytest.mark.parametrize( 98 | "method", 99 | [ 100 | BaseChannelLayer().require_valid_channel_name, 101 | BaseChannelLayer().require_valid_group_name, 102 | ], 103 | ) 104 | @pytest.mark.parametrize( 105 | "channel_name,expected_valid", 106 | [("¯\\_(ツ)_/¯", False), ("chat", True), ("chat" * 100, False)], 107 | ) 108 | def test_channel_and_group_name_validation(method, channel_name, expected_valid): 109 | if expected_valid: 110 | method(channel_name) 111 | else: 112 | with pytest.raises(TypeError): 113 | method(channel_name) 114 | 115 | 116 | @pytest.mark.parametrize( 117 | "name", 118 | [ 119 | "a" * 101, # Group name too long 120 | ], 121 | ) 122 | def test_group_name_length_error_message(name): 123 | """ 124 | Ensure the correct error message is raised when group names 125 | exceed the character limit or contain invalid characters. 126 | """ 127 | layer = BaseChannelLayer() 128 | expected_error_message = layer.invalid_name_error.format("Group") 129 | 130 | with pytest.raises(TypeError, match=expected_error_message): 131 | layer.require_valid_group_name(name) 132 | 133 | 134 | @pytest.mark.parametrize( 135 | "name", 136 | [ 137 | "a" * 101, # Channel name too long 138 | ], 139 | ) 140 | def test_channel_name_length_error_message(name): 141 | """ 142 | Ensure the correct error message is raised when group names 143 | exceed the character limit or contain invalid characters. 144 | """ 145 | layer = BaseChannelLayer() 146 | expected_error_message = layer.invalid_name_error.format("Channel") 147 | 148 | with pytest.raises(TypeError, match=expected_error_message): 149 | layer.require_valid_channel_name(name) 150 | 151 | 152 | def test_deprecated_valid_channel_name(): 153 | """ 154 | Test that the deprecated valid_channel_name method works 155 | but raises a deprecation warning. 156 | """ 157 | layer = BaseChannelLayer() 158 | 159 | # Should work with valid name but raise warning 160 | with pytest.warns(DeprecationWarning, match="valid_channel_name is deprecated"): 161 | assert layer.valid_channel_name("valid-channel") 162 | 163 | # Should raise TypeError for invalid names 164 | with pytest.warns(DeprecationWarning): 165 | with pytest.raises(TypeError): 166 | layer.valid_channel_name("¯\\_(ツ)_/¯") 167 | 168 | 169 | def test_deprecated_valid_group_name(): 170 | """ 171 | Test that the deprecated valid_group_name method works 172 | but raises a deprecation warning. 173 | """ 174 | layer = BaseChannelLayer() 175 | 176 | # Should work with valid name but raise warning 177 | with pytest.warns(DeprecationWarning, match="valid_group_name is deprecated"): 178 | assert layer.valid_group_name("valid-group") 179 | 180 | # Should raise TypeError for invalid names 181 | with pytest.warns(DeprecationWarning): 182 | with pytest.raises(TypeError): 183 | layer.valid_group_name("¯\\_(ツ)_/¯") 184 | -------------------------------------------------------------------------------- /channels/security/websocket.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | 3 | from django.conf import settings 4 | from django.http.request import is_same_domain 5 | 6 | from ..generic.websocket import AsyncWebsocketConsumer 7 | 8 | 9 | class OriginValidator: 10 | """ 11 | Validates that the incoming connection has an Origin header that 12 | is in an allowed list. 13 | """ 14 | 15 | def __init__(self, application, allowed_origins): 16 | self.application = application 17 | self.allowed_origins = allowed_origins 18 | 19 | async def __call__(self, scope, receive, send): 20 | # Make sure the scope is of type websocket 21 | if scope["type"] != "websocket": 22 | raise ValueError( 23 | "You cannot use OriginValidator on a non-WebSocket connection" 24 | ) 25 | # Extract the Origin header 26 | parsed_origin = None 27 | for header_name, header_value in scope.get("headers", []): 28 | if header_name == b"origin": 29 | try: 30 | # Set ResultParse 31 | parsed_origin = urlparse(header_value.decode("latin1")) 32 | except UnicodeDecodeError: 33 | pass 34 | # Check to see if the origin header is valid 35 | if self.valid_origin(parsed_origin): 36 | # Pass control to the application 37 | return await self.application(scope, receive, send) 38 | else: 39 | # Deny the connection 40 | denier = WebsocketDenier() 41 | return await denier(scope, receive, send) 42 | 43 | def valid_origin(self, parsed_origin): 44 | """ 45 | Checks parsed origin is None. 46 | 47 | Pass control to the validate_origin function. 48 | 49 | Returns ``True`` if validation function was successful, ``False`` otherwise. 50 | """ 51 | # None is not allowed unless all hosts are allowed 52 | if parsed_origin is None and "*" not in self.allowed_origins: 53 | return False 54 | return self.validate_origin(parsed_origin) 55 | 56 | def validate_origin(self, parsed_origin): 57 | """ 58 | Validate the given origin for this site. 59 | 60 | Check than the origin looks valid and matches the origin pattern in 61 | specified list ``allowed_origins``. Any pattern begins with a scheme. 62 | After the scheme there must be a domain. Any domain beginning with a 63 | period corresponds to the domain and all its subdomains (for example, 64 | ``http://.example.com``). After the domain there must be a port, 65 | but it can be omitted. ``*`` matches anything and anything 66 | else must match exactly. 67 | 68 | Note. This function assumes that the given origin has a schema, domain 69 | and port, but port is optional. 70 | 71 | Returns ``True`` for a valid host, ``False`` otherwise. 72 | """ 73 | return any( 74 | pattern == "*" or self.match_allowed_origin(parsed_origin, pattern) 75 | for pattern in self.allowed_origins 76 | ) 77 | 78 | def match_allowed_origin(self, parsed_origin, pattern): 79 | """ 80 | Returns ``True`` if the origin is either an exact match or a match 81 | to the wildcard pattern. Compares scheme, domain, port of origin and pattern. 82 | 83 | Any pattern can be begins with a scheme. After the scheme must be a domain, 84 | or just domain without scheme. 85 | Any domain beginning with a period corresponds to the domain and all 86 | its subdomains (for example, ``.example.com`` ``example.com`` 87 | and any subdomain). Also with scheme (for example, ``http://.example.com`` 88 | ``http://example.com``). After the domain there must be a port, 89 | but it can be omitted. 90 | 91 | Note. This function assumes that the given origin is either None, a 92 | schema-domain-port string, or just a domain string 93 | """ 94 | if parsed_origin is None: 95 | return False 96 | 97 | # Get ResultParse object 98 | parsed_pattern = urlparse(pattern.lower()) 99 | if parsed_origin.hostname is None: 100 | return False 101 | if not parsed_pattern.scheme: 102 | pattern_hostname = urlparse("//" + pattern).hostname or pattern 103 | return is_same_domain(parsed_origin.hostname, pattern_hostname) 104 | # Get origin.port or default ports for origin or None 105 | origin_port = self.get_origin_port(parsed_origin) 106 | # Get pattern.port or default ports for pattern or None 107 | pattern_port = self.get_origin_port(parsed_pattern) 108 | # Compares hostname, scheme, ports of pattern and origin 109 | if ( 110 | parsed_pattern.scheme == parsed_origin.scheme 111 | and origin_port == pattern_port 112 | and is_same_domain(parsed_origin.hostname, parsed_pattern.hostname) 113 | ): 114 | return True 115 | return False 116 | 117 | def get_origin_port(self, origin): 118 | """ 119 | Returns the origin.port or port for this schema by default. 120 | Otherwise, it returns None. 121 | """ 122 | if origin.port is not None: 123 | # Return origin.port 124 | return origin.port 125 | # if origin.port doesn`t exists 126 | if origin.scheme == "http" or origin.scheme == "ws": 127 | # Default port return for http, ws 128 | return 80 129 | elif origin.scheme == "https" or origin.scheme == "wss": 130 | # Default port return for https, wss 131 | return 443 132 | else: 133 | return None 134 | 135 | 136 | def AllowedHostsOriginValidator(application): 137 | """ 138 | Factory function which returns an OriginValidator configured to use 139 | settings.ALLOWED_HOSTS. 140 | """ 141 | allowed_hosts = settings.ALLOWED_HOSTS 142 | if settings.DEBUG and not allowed_hosts: 143 | allowed_hosts = ["localhost", "127.0.0.1", "[::1]"] 144 | return OriginValidator(application, allowed_hosts) 145 | 146 | 147 | class WebsocketDenier(AsyncWebsocketConsumer): 148 | """ 149 | Simple application which denies all requests to it. 150 | """ 151 | 152 | async def connect(self): 153 | await self.close() 154 | -------------------------------------------------------------------------------- /docs/topics/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | Channels supports standard Django authentication out-of-the-box for HTTP and 5 | WebSocket consumers, and you can write your own middleware or handling code 6 | if you want to support a different authentication scheme (for example, 7 | tokens in the URL). 8 | 9 | 10 | Django authentication 11 | --------------------- 12 | 13 | The ``AuthMiddleware`` in Channels supports standard Django authentication, 14 | where the user details are stored in the session. It allows read-only access 15 | to a user object in the ``scope``. 16 | 17 | ``AuthMiddleware`` requires ``SessionMiddleware`` to function, which itself 18 | requires ``CookieMiddleware``. For convenience, these are also provided 19 | as a combined callable called ``AuthMiddlewareStack`` that includes all three. 20 | 21 | To use the middleware, wrap it around the appropriate level of consumer 22 | in your ``asgi.py``: 23 | 24 | .. code-block:: python 25 | 26 | from django.urls import re_path 27 | 28 | from channels.routing import ProtocolTypeRouter, URLRouter 29 | from channels.auth import AuthMiddlewareStack 30 | from channels.security.websocket import AllowedHostsOriginValidator 31 | 32 | from myapp import consumers 33 | 34 | application = ProtocolTypeRouter({ 35 | 36 | "websocket": AllowedHostsOriginValidator( 37 | AuthMiddlewareStack( 38 | URLRouter([ 39 | re_path(r"^front(end)/$", consumers.AsyncChatConsumer.as_asgi()), 40 | ]) 41 | ) 42 | ), 43 | 44 | }) 45 | 46 | While you can wrap the middleware around each consumer individually, 47 | it's recommended you wrap it around a higher-level application component, 48 | like in this case the ``URLRouter``. 49 | 50 | Note that the ``AuthMiddleware`` will only work on protocols that provide 51 | HTTP headers in their ``scope`` - by default, this is HTTP and WebSocket. 52 | 53 | To access the user, just use ``self.scope["user"]`` in your consumer code: 54 | 55 | .. code-block:: python 56 | 57 | class ChatConsumer(WebsocketConsumer): 58 | 59 | def connect(self): 60 | self.user = self.scope["user"] 61 | self.accept() 62 | 63 | 64 | Custom Authentication 65 | --------------------- 66 | 67 | If you have a custom authentication scheme, you can write a custom middleware 68 | to parse the details and put a user object (or whatever other object you need) 69 | into your scope. 70 | 71 | Middleware is written as a callable that takes an ASGI application and wraps 72 | it to return another ASGI application. Most authentication can just be done 73 | on the scope, so all you need to do is override the initial constructor 74 | that takes a scope, rather than the event-running coroutine. 75 | 76 | Here's a simple example of a middleware that just takes a user ID out of the 77 | query string and uses that: 78 | 79 | .. code-block:: python 80 | 81 | from channels.db import database_sync_to_async 82 | 83 | @database_sync_to_async 84 | def get_user(user_id): 85 | try: 86 | return User.objects.get(id=user_id) 87 | except User.DoesNotExist: 88 | return AnonymousUser() 89 | 90 | class QueryAuthMiddleware: 91 | """ 92 | Custom middleware (insecure) that takes user IDs from the query string. 93 | """ 94 | 95 | def __init__(self, app): 96 | # Store the ASGI application we were passed 97 | self.app = app 98 | 99 | async def __call__(self, scope, receive, send): 100 | # Look up user from query string (you should also do things like 101 | # checking if it is a valid user ID, or if scope["user"] is already 102 | # populated). 103 | scope['user'] = await get_user(int(scope["query_string"])) 104 | 105 | return await self.app(scope, receive, send) 106 | 107 | The same principles can be applied to authenticate over non-HTTP protocols; 108 | for example, you might want to use someone's chat username from a chat protocol 109 | to turn it into a user. 110 | 111 | 112 | How to log a user in/out 113 | ------------------------ 114 | 115 | Channels provides direct login and logout functions (much like Django's 116 | ``contrib.auth`` package does) as ``channels.auth.login`` and 117 | ``channels.auth.logout``. 118 | 119 | Within your consumer you can await ``login(scope, user, backend=None)`` 120 | to log a user in. This requires that your scope has a ``session`` object; 121 | the best way to do this is to ensure your consumer is wrapped in a 122 | ``SessionMiddlewareStack`` or a ``AuthMiddlewareStack``. 123 | 124 | You can logout a user with the ``logout(scope)`` async function. 125 | 126 | If you are in a WebSocket consumer, or logging-in after the first response 127 | has been sent in a http consumer, the session is populated 128 | **but will not be saved automatically** - you must call 129 | ``scope["session"].save()`` after login in your consumer code: 130 | 131 | .. code-block:: python 132 | 133 | from channels.auth import login 134 | 135 | class ChatConsumer(AsyncWebsocketConsumer): 136 | 137 | ... 138 | 139 | async def receive(self, text_data): 140 | ... 141 | # login the user to this session. 142 | await login(self.scope, user) 143 | # save the session (if the session backend does not access the db you can use `sync_to_async`) 144 | await database_sync_to_async(self.scope["session"].save)() 145 | 146 | When calling ``login(scope, user)``, ``logout(scope)`` or ``get_user(scope)`` 147 | from a synchronous function you will need to wrap them in ``async_to_sync``, 148 | as we only provide async versions: 149 | 150 | .. code-block:: python 151 | 152 | from asgiref.sync import async_to_sync 153 | from channels.auth import login 154 | 155 | class SyncChatConsumer(WebsocketConsumer): 156 | 157 | ... 158 | 159 | def receive(self, text_data): 160 | ... 161 | async_to_sync(login)(self.scope, user) 162 | self.scope["session"].save() 163 | 164 | .. note:: 165 | 166 | If you are using a long running consumer, websocket or long-polling 167 | HTTP it is possible that the user will be logged out of their session 168 | elsewhere while your consumer is running. You can periodically use 169 | ``get_user(scope)`` to be sure that the user is still logged in. 170 | -------------------------------------------------------------------------------- /channels/routing.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from django.conf import settings 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.urls.exceptions import Resolver404 6 | from django.urls.resolvers import RegexPattern, RoutePattern, URLResolver 7 | 8 | """ 9 | All Routing instances inside this file are also valid ASGI applications - with 10 | new Channels routing, whatever you end up with as the top level object is just 11 | served up as the "ASGI application". 12 | """ 13 | 14 | 15 | def get_default_application(): 16 | """ 17 | Gets the default application, set in the ASGI_APPLICATION setting. 18 | """ 19 | try: 20 | path, name = settings.ASGI_APPLICATION.rsplit(".", 1) 21 | except (ValueError, AttributeError): 22 | raise ImproperlyConfigured("Cannot find ASGI_APPLICATION setting.") 23 | try: 24 | module = importlib.import_module(path) 25 | except ImportError: 26 | raise ImproperlyConfigured("Cannot import ASGI_APPLICATION module %r" % path) 27 | try: 28 | value = getattr(module, name) 29 | except AttributeError: 30 | raise ImproperlyConfigured( 31 | "Cannot find %r in ASGI_APPLICATION module %s" % (name, path) 32 | ) 33 | return value 34 | 35 | 36 | class ProtocolTypeRouter: 37 | """ 38 | Takes a mapping of protocol type names to other Application instances, 39 | and dispatches to the right one based on protocol name (or raises an error) 40 | """ 41 | 42 | def __init__(self, application_mapping): 43 | self.application_mapping = application_mapping 44 | 45 | async def __call__(self, scope, receive, send): 46 | if scope["type"] in self.application_mapping: 47 | application = self.application_mapping[scope["type"]] 48 | return await application(scope, receive, send) 49 | else: 50 | raise ValueError( 51 | "No application configured for scope type %r" % scope["type"] 52 | ) 53 | 54 | 55 | class URLRouter: 56 | """ 57 | Routes to different applications/consumers based on the URL path. 58 | 59 | Works with anything that has a ``path`` key, but intended for WebSocket 60 | and HTTP. Uses Django's django.urls objects for resolution - 61 | path() or re_path(). 62 | """ 63 | 64 | #: This router wants to do routing based on scope[path] or 65 | #: scope[path_remaining]. ``path()`` entries in URLRouter should not be 66 | #: treated as endpoints (ended with ``$``), but similar to ``include()``. 67 | _path_routing = True 68 | 69 | def __init__(self, routes): 70 | self.routes = routes 71 | 72 | for route in self.routes: 73 | # The inner ASGI app wants to do additional routing, route 74 | # must not be an endpoint 75 | if getattr(route.callback, "_path_routing", False) is True: 76 | pattern = route.pattern 77 | if isinstance(pattern, RegexPattern): 78 | arg = pattern._regex 79 | elif isinstance(pattern, RoutePattern): 80 | arg = pattern._route 81 | else: 82 | raise ValueError(f"Unsupported pattern type: {type(pattern)}") 83 | route.pattern = pattern.__class__(arg, pattern.name, is_endpoint=False) 84 | 85 | if not route.callback and isinstance(route, URLResolver): 86 | raise ImproperlyConfigured( 87 | "%s: include() is not supported in URLRouter. Use nested" 88 | " URLRouter instances instead." % (route,) 89 | ) 90 | 91 | async def __call__(self, scope, receive, send): 92 | # Get the path 93 | path = scope.get("path_remaining", scope.get("path", None)) 94 | if path is None: 95 | raise ValueError("No 'path' key in connection scope, cannot route URLs") 96 | 97 | if "path_remaining" not in scope: 98 | # We are the outermost URLRouter, so handle root_path if present. 99 | root_path = scope.get("root_path", "") 100 | if root_path and not path.startswith(root_path): 101 | # If root_path is present, path must start with it. 102 | raise ValueError("No route found for path %r." % path) 103 | path = path[len(root_path) :] 104 | 105 | # Remove leading / to match Django's handling 106 | path = path.lstrip("/") 107 | # Run through the routes we have until one matches 108 | for route in self.routes: 109 | try: 110 | match = route.pattern.match(path) 111 | if match: 112 | new_path, args, kwargs = match 113 | # Add defaults to kwargs from the URL pattern. 114 | kwargs.update(route.default_args) 115 | # Add args or kwargs into the scope 116 | outer = scope.get("url_route", {}) 117 | application = route.callback 118 | return await application( 119 | dict( 120 | scope, 121 | path_remaining=new_path, 122 | url_route={ 123 | "args": outer.get("args", ()) + args, 124 | "kwargs": {**outer.get("kwargs", {}), **kwargs}, 125 | }, 126 | ), 127 | receive, 128 | send, 129 | ) 130 | except Resolver404: 131 | pass 132 | else: 133 | if "path_remaining" in scope: 134 | raise Resolver404("No route found for path %r." % path) 135 | # We are the outermost URLRouter 136 | raise ValueError("No route found for path %r." % path) 137 | 138 | 139 | class ChannelNameRouter: 140 | """ 141 | Maps to different applications based on a "channel" key in the scope 142 | (intended for the Channels worker mode) 143 | """ 144 | 145 | def __init__(self, application_mapping): 146 | self.application_mapping = application_mapping 147 | 148 | async def __call__(self, scope, receive, send): 149 | if "channel" not in scope: 150 | raise ValueError( 151 | "ChannelNameRouter got a scope without a 'channel' key. " 152 | + "Did you make sure it's only being used for 'channel' type messages?" 153 | ) 154 | if scope["channel"] in self.application_mapping: 155 | application = self.application_mapping[scope["channel"]] 156 | return await application(scope, receive, send) 157 | else: 158 | raise ValueError( 159 | "No application configured for channel name %r" % scope["channel"] 160 | ) 161 | -------------------------------------------------------------------------------- /tests/test_testing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from urllib.parse import unquote 3 | 4 | import pytest 5 | from django.urls import path 6 | 7 | from channels.consumer import AsyncConsumer 8 | from channels.generic.websocket import WebsocketConsumer 9 | from channels.routing import URLRouter 10 | from channels.testing import HttpCommunicator, WebsocketCommunicator 11 | 12 | 13 | class SimpleHttpApp(AsyncConsumer): 14 | """ 15 | Barebones HTTP ASGI app for testing. 16 | """ 17 | 18 | async def http_request(self, event): 19 | assert self.scope["path"] == "/test/" 20 | assert self.scope["method"] == "GET" 21 | assert self.scope["query_string"] == b"foo=bar" 22 | await self.send({"type": "http.response.start", "status": 200, "headers": []}) 23 | await self.send({"type": "http.response.body", "body": b"test response"}) 24 | 25 | 26 | @pytest.mark.django_db(transaction=True) 27 | @pytest.mark.asyncio 28 | async def test_http_communicator(): 29 | """ 30 | Tests that the HTTP communicator class works at a basic level. 31 | """ 32 | communicator = HttpCommunicator(SimpleHttpApp(), "GET", "/test/?foo=bar") 33 | response = await communicator.get_response() 34 | assert response["body"] == b"test response" 35 | assert response["status"] == 200 36 | 37 | 38 | class SimpleWebsocketApp(WebsocketConsumer): 39 | """ 40 | Barebones WebSocket ASGI app for testing. 41 | """ 42 | 43 | def connect(self): 44 | assert self.scope["path"] == "/testws/" 45 | self.accept() 46 | 47 | def receive(self, text_data=None, bytes_data=None): 48 | self.send(text_data=text_data, bytes_data=bytes_data) 49 | 50 | 51 | class AcceptCloseWebsocketApp(WebsocketConsumer): 52 | def connect(self): 53 | assert self.scope["path"] == "/testws/" 54 | self.accept() 55 | self.close() 56 | 57 | 58 | class ErrorWebsocketApp(WebsocketConsumer): 59 | """ 60 | Barebones WebSocket ASGI app for error testing. 61 | """ 62 | 63 | def receive(self, text_data=None, bytes_data=None): 64 | pass 65 | 66 | 67 | class KwargsWebSocketApp(WebsocketConsumer): 68 | """ 69 | WebSocket ASGI app used for testing the kwargs arguments in the url_route. 70 | """ 71 | 72 | def connect(self): 73 | self.accept() 74 | self.send(text_data=self.scope["url_route"]["kwargs"]["message"]) 75 | 76 | 77 | @pytest.mark.django_db(transaction=True) 78 | @pytest.mark.asyncio 79 | async def test_websocket_communicator(): 80 | """ 81 | Tests that the WebSocket communicator class works at a basic level. 82 | """ 83 | communicator = WebsocketCommunicator(SimpleWebsocketApp(), "/testws/") 84 | # Test connection 85 | connected, subprotocol = await communicator.connect() 86 | assert connected 87 | assert subprotocol is None 88 | # Test sending text 89 | await communicator.send_to(text_data="hello") 90 | response = await communicator.receive_from() 91 | assert response == "hello" 92 | # Test sending bytes 93 | await communicator.send_to(bytes_data=b"w\0\0\0") 94 | response = await communicator.receive_from() 95 | assert response == b"w\0\0\0" 96 | # Test sending JSON 97 | await communicator.send_json_to({"hello": "world"}) 98 | response = await communicator.receive_json_from() 99 | assert response == {"hello": "world"} 100 | # Close out 101 | await communicator.disconnect() 102 | 103 | 104 | @pytest.mark.django_db(transaction=True) 105 | @pytest.mark.asyncio 106 | async def test_websocket_incorrect_read_json(): 107 | """ 108 | When using an invalid communicator method, an assertion error will be raised with 109 | informative message. 110 | In this test, the server accepts and then immediately closes the connection so 111 | the server is not in a valid state to handle "receive_from". 112 | """ 113 | communicator = WebsocketCommunicator(AcceptCloseWebsocketApp(), "/testws/") 114 | await communicator.connect() 115 | with pytest.raises(AssertionError) as exception_info: 116 | await communicator.receive_from() 117 | assert ( 118 | str(exception_info.value) 119 | == "Expected type 'websocket.send', but was 'websocket.close'" 120 | ) 121 | 122 | 123 | @pytest.mark.django_db(transaction=True) 124 | @pytest.mark.asyncio 125 | async def test_websocket_application(): 126 | """ 127 | Tests that the WebSocket communicator class works with the 128 | URLRoute application. 129 | """ 130 | application = URLRouter([path("testws//", KwargsWebSocketApp())]) 131 | communicator = WebsocketCommunicator(application, "/testws/test/") 132 | connected, subprotocol = await communicator.connect() 133 | # Test connection 134 | assert connected 135 | assert subprotocol is None 136 | message = await communicator.receive_from() 137 | assert message == "test" 138 | await communicator.disconnect() 139 | 140 | 141 | @pytest.mark.django_db(transaction=True) 142 | @pytest.mark.asyncio 143 | async def test_timeout_disconnect(): 144 | """ 145 | Tests that communicator.disconnect() raises after a timeout. (Application 146 | is finished.) 147 | """ 148 | communicator = WebsocketCommunicator(ErrorWebsocketApp(), "/testws/") 149 | # Test connection 150 | connected, subprotocol = await communicator.connect() 151 | assert connected 152 | assert subprotocol is None 153 | # Test sending text (will error internally) 154 | await communicator.send_to(text_data="hello") 155 | with pytest.raises(asyncio.TimeoutError): 156 | await communicator.receive_from() 157 | 158 | with pytest.raises(asyncio.exceptions.CancelledError): 159 | await communicator.disconnect() 160 | 161 | 162 | class ConnectionScopeValidator(WebsocketConsumer): 163 | """ 164 | Tests ASGI specification for the connection scope. 165 | """ 166 | 167 | def connect(self): 168 | assert self.scope["type"] == "websocket" 169 | # check if path is a unicode string 170 | assert isinstance(self.scope["path"], str) 171 | # check if path has percent escapes decoded 172 | assert self.scope["path"] == unquote(self.scope["path"]) 173 | # check if query_string is a bytes sequence 174 | assert isinstance(self.scope["query_string"], bytes) 175 | self.accept() 176 | 177 | 178 | paths = [ 179 | "user:pass@example.com:8080/p/a/t/h?query=string#hash", 180 | "wss://user:pass@example.com:8080/p/a/t/h?query=string#hash", 181 | ( 182 | "ws://www.example.com/%E9%A6%96%E9%A1%B5/index.php?" 183 | "foo=%E9%A6%96%E9%A1%B5&spam=eggs" 184 | ), 185 | ] 186 | 187 | 188 | @pytest.mark.django_db(transaction=True) 189 | @pytest.mark.asyncio 190 | @pytest.mark.parametrize("path", paths) 191 | async def test_connection_scope(path): 192 | """ 193 | Tests ASGI specification for the the connection scope. 194 | """ 195 | communicator = WebsocketCommunicator(ConnectionScopeValidator(), path) 196 | connected, _ = await communicator.connect() 197 | assert connected 198 | await communicator.disconnect() 199 | --------------------------------------------------------------------------------