├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------