mirror of
https://github.com/django/daphne.git
synced 2025-04-20 08:42:18 +03:00
Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7c9334952d | ||
|
630caed915 | ||
|
786e4c120a | ||
|
f0a3ec60e9 | ||
|
32ac73e1a0 | ||
|
e25b4bcc31 | ||
|
06afd9b94a | ||
|
ffad9c3cfd | ||
|
862ebcd08d | ||
|
b1902e8ccd | ||
|
9ec5798c0d | ||
|
3607351212 | ||
|
420f065d9e | ||
|
4a55faca45 | ||
|
0f15e4595b | ||
|
cf9145985b | ||
|
c0b630834c | ||
|
63790936d1 | ||
|
df0680c9ad | ||
|
ef24796243 | ||
|
9a282dd627 | ||
|
5fdc9176e5 | ||
|
993efe62ce | ||
|
c07925d53f | ||
|
4d24e22c72 | ||
|
2d4dcbf149 | ||
|
3758c514fd | ||
|
f108bbc7c1 | ||
|
e49c39a4e5 | ||
|
1eaf2206cc | ||
|
09da15dc4e | ||
|
21513b84da | ||
|
077dd81809 | ||
|
b0204165b1 | ||
|
79fd65dec3 | ||
|
5681d71c17 | ||
|
fdc944a280 | ||
|
c9343aa9d6 | ||
|
d59c2bd424 | ||
|
2015ecdd8f | ||
|
18e936eed1 | ||
|
ef946d5637 | ||
|
a0b2ac0e8f | ||
|
afd0d51b83 | ||
|
d5fbdfc4cb | ||
|
71be46265d | ||
|
91c61f4ff4 |
11
.flake8
Normal file
11
.flake8
Normal file
|
@ -0,0 +1,11 @@
|
|||
[flake8]
|
||||
exclude =
|
||||
.venv,
|
||||
.tox,
|
||||
docs,
|
||||
testproject,
|
||||
js_client,
|
||||
.eggs
|
||||
|
||||
extend-ignore = E123, E128, E266, E402, W503, E731, W601, B036
|
||||
max-line-length = 120
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
17
.github/workflows/tests.yml
vendored
17
.github/workflows/tests.yml
vendored
|
@ -5,6 +5,10 @@ on:
|
|||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
@ -16,23 +20,24 @@ jobs:
|
|||
- ubuntu
|
||||
- windows
|
||||
python-version:
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
python -m pip install --upgrade tox tox-py
|
||||
python -m pip install --upgrade tox
|
||||
|
||||
- name: Run tox targets for ${{ matrix.python-version }}
|
||||
run: tox --py current
|
||||
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,3 +13,4 @@ test_consumer*
|
|||
.python-version
|
||||
.pytest_cache/
|
||||
.vscode
|
||||
.coverage
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.38.2
|
||||
rev: v3.19.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py36-plus]
|
||||
args: [--py39-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.8.0
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
rev: 6.0.1
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
rev: 7.2.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
|
|
|
@ -1,3 +1,37 @@
|
|||
4.2.0 (to be released)
|
||||
------------------
|
||||
|
||||
* Added support for Python 3.13.
|
||||
|
||||
* Drop support for EOL Python 3.8.
|
||||
|
||||
* Removed unused pytest-runner
|
||||
|
||||
* Fixed sdist file to ensure it includes all tests
|
||||
|
||||
4.1.2 (2024-04-11)
|
||||
------------------
|
||||
|
||||
* Fixed a setuptools configuration error in 4.1.1.
|
||||
|
||||
4.1.1 (2024-04-10)
|
||||
------------------
|
||||
|
||||
* Fixed a twisted.plugin packaging error in 4.1.0.
|
||||
|
||||
Thanks to sdc50.
|
||||
|
||||
4.1.0 (2024-02-10)
|
||||
------------------
|
||||
|
||||
* Added support for Python 3.12.
|
||||
|
||||
* Dropped support for EOL Python 3.7.
|
||||
|
||||
* Handled root path for websocket scopes.
|
||||
|
||||
* Validate HTTP header names as per RFC 9110.
|
||||
|
||||
4.0.0 (2022-10-07)
|
||||
------------------
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
include LICENSE
|
||||
recursive-include tests *.py
|
|
@ -54,7 +54,7 @@ Daphne supports terminating HTTP/2 connections natively. You'll
|
|||
need to do a couple of things to get it working, though. First, you need to
|
||||
make sure you install the Twisted ``http2`` and ``tls`` extras::
|
||||
|
||||
pip install -U 'Twisted[tls,http2]'
|
||||
pip install -U "Twisted[tls,http2]"
|
||||
|
||||
Next, because all current browsers only support HTTP/2 when using TLS, you will
|
||||
need to start Daphne with TLS turned on, which can be done using the Twisted endpoint syntax::
|
||||
|
@ -108,7 +108,7 @@ should start with a slash, but not end with one; for example::
|
|||
Python Support
|
||||
--------------
|
||||
|
||||
Daphne requires Python 3.7 or later.
|
||||
Daphne requires Python 3.9 or later.
|
||||
|
||||
|
||||
Contributing
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import sys
|
||||
|
||||
__version__ = "4.0.0"
|
||||
__version__ = "4.1.3"
|
||||
|
||||
|
||||
# Windows on Python 3.8+ uses ProactorEventLoop, which is not compatible with
|
||||
|
|
|
@ -270,16 +270,16 @@ class CommandLineInterface:
|
|||
websocket_connect_timeout=args.websocket_connect_timeout,
|
||||
websocket_handshake_timeout=args.websocket_connect_timeout,
|
||||
application_close_timeout=args.application_close_timeout,
|
||||
action_logger=AccessLogGenerator(access_log_stream)
|
||||
if access_log_stream
|
||||
else None,
|
||||
action_logger=(
|
||||
AccessLogGenerator(access_log_stream) if access_log_stream else None
|
||||
),
|
||||
root_path=args.root_path,
|
||||
verbosity=args.verbosity,
|
||||
proxy_forwarded_address_header=self._get_forwarded_host(args=args),
|
||||
proxy_forwarded_port_header=self._get_forwarded_port(args=args),
|
||||
proxy_forwarded_proto_header="X-Forwarded-Proto"
|
||||
if args.proxy_headers
|
||||
else None,
|
||||
proxy_forwarded_proto_header=(
|
||||
"X-Forwarded-Proto" if args.proxy_headers else None
|
||||
),
|
||||
server_name=args.server_name,
|
||||
)
|
||||
self.server.run()
|
||||
|
|
|
@ -9,7 +9,7 @@ from twisted.protocols.policies import ProtocolWrapper
|
|||
from twisted.web import http
|
||||
from zope.interface import implementer
|
||||
|
||||
from .utils import parse_x_forwarded_for
|
||||
from .utils import HEADER_NAME_RE, parse_x_forwarded_for
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -69,6 +69,13 @@ class WebRequest(http.Request):
|
|||
def process(self):
|
||||
try:
|
||||
self.request_start = time.time()
|
||||
|
||||
# Validate header names.
|
||||
for name, _ in self.requestHeaders.getAllRawHeaders():
|
||||
if not HEADER_NAME_RE.fullmatch(name):
|
||||
self.basic_error(400, b"Bad Request", "Invalid header name")
|
||||
return
|
||||
|
||||
# Get upgrade header
|
||||
upgrade_header = None
|
||||
if self.requestHeaders.hasHeader(b"Upgrade"):
|
||||
|
@ -279,9 +286,11 @@ class WebRequest(http.Request):
|
|||
"path": uri,
|
||||
"status": self.code,
|
||||
"method": self.method.decode("ascii", "replace"),
|
||||
"client": "%s:%s" % tuple(self.client_addr)
|
||||
"client": (
|
||||
"%s:%s" % tuple(self.client_addr)
|
||||
if self.client_addr
|
||||
else None,
|
||||
else None
|
||||
),
|
||||
"time_taken": self.duration(),
|
||||
"size": self.sentLength,
|
||||
},
|
||||
|
|
|
@ -73,6 +73,18 @@ class Command(RunserverCommand):
|
|||
"seconds (default: 5)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nostatic",
|
||||
action="store_false",
|
||||
dest="use_static_handler",
|
||||
help="Tells Django to NOT automatically serve static files at STATIC_URL.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--insecure",
|
||||
action="store_true",
|
||||
dest="insecure_serving",
|
||||
help="Allows serving static files even if DEBUG is False.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.http_timeout = options.get("http_timeout", None)
|
||||
|
|
|
@ -21,6 +21,7 @@ if current_reactor is not None:
|
|||
+ "you can fix this warning by importing daphne.server early in your codebase or "
|
||||
+ "finding the package that imports Twisted and importing it later on.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
del sys.modules["twisted.internet.reactor"]
|
||||
asyncioreactor.install(twisted_loop)
|
||||
|
|
|
@ -158,7 +158,7 @@ class DaphneProcess(multiprocessing.Process):
|
|||
application=application,
|
||||
endpoints=endpoints,
|
||||
signal_handlers=False,
|
||||
**self.kwargs
|
||||
**self.kwargs,
|
||||
)
|
||||
# Set up a poller to look for the port
|
||||
reactor.callLater(0.1, self.resolve_port)
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import importlib
|
||||
import re
|
||||
|
||||
from twisted.web.http_headers import Headers
|
||||
|
||||
# Header name regex as per h11.
|
||||
# https://github.com/python-hyper/h11/blob/a2c68948accadc3876dffcf979d98002e4a4ed27/h11/_abnf.py#L10-L21
|
||||
HEADER_NAME_RE = re.compile(rb"[-!#$%&'*+.^_`|~0-9a-zA-Z]+")
|
||||
|
||||
|
||||
def import_by_path(path):
|
||||
"""
|
||||
|
|
|
@ -31,16 +31,20 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|||
self.server.protocol_connected(self)
|
||||
self.request = request
|
||||
self.protocol_to_accept = None
|
||||
self.root_path = self.server.root_path
|
||||
self.socket_opened = time.time()
|
||||
self.last_ping = time.time()
|
||||
try:
|
||||
# Sanitize and decode headers
|
||||
# Sanitize and decode headers, potentially extracting root path
|
||||
self.clean_headers = []
|
||||
for name, value in request.headers.items():
|
||||
name = name.encode("ascii")
|
||||
# Prevent CVE-2015-0219
|
||||
if b"_" in name:
|
||||
continue
|
||||
if name.lower() == b"daphne-root-path":
|
||||
self.root_path = unquote(value)
|
||||
else:
|
||||
self.clean_headers.append((name.lower(), value.encode("latin1")))
|
||||
# Get client address if possible
|
||||
peer = self.transport.getPeer()
|
||||
|
@ -76,6 +80,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|||
"type": "websocket",
|
||||
"path": unquote(self.path.decode("ascii")),
|
||||
"raw_path": self.path,
|
||||
"root_path": self.root_path,
|
||||
"headers": self.clean_headers,
|
||||
"query_string": self._raw_query_string, # Passed by HTTP protocol
|
||||
"client": self.client_addr,
|
||||
|
@ -110,9 +115,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|||
"connecting",
|
||||
{
|
||||
"path": self.request.path,
|
||||
"client": "%s:%s" % tuple(self.client_addr)
|
||||
if self.client_addr
|
||||
else None,
|
||||
"client": (
|
||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -133,9 +138,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|||
"connected",
|
||||
{
|
||||
"path": self.request.path,
|
||||
"client": "%s:%s" % tuple(self.client_addr)
|
||||
if self.client_addr
|
||||
else None,
|
||||
"client": (
|
||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -170,9 +175,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|||
"disconnected",
|
||||
{
|
||||
"path": self.request.path,
|
||||
"client": "%s:%s" % tuple(self.client_addr)
|
||||
if self.client_addr
|
||||
else None,
|
||||
"client": (
|
||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -237,9 +242,9 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|||
"rejected",
|
||||
{
|
||||
"path": self.request.path,
|
||||
"client": "%s:%s" % tuple(self.client_addr)
|
||||
if self.client_addr
|
||||
else None,
|
||||
"client": (
|
||||
"%s:%s" % tuple(self.client_addr) if self.client_addr else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
81
pyproject.toml
Normal file
81
pyproject.toml
Normal file
|
@ -0,0 +1,81 @@
|
|||
[project]
|
||||
name = "daphne"
|
||||
dynamic = ["version"]
|
||||
description = "Django ASGI (HTTP/WebSocket) server"
|
||||
requires-python = ">=3.9"
|
||||
authors = [
|
||||
{ name = "Django Software Foundation", email = "foundation@djangoproject.com" },
|
||||
]
|
||||
|
||||
license = { text = "BSD" }
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
]
|
||||
|
||||
dependencies = ["asgiref>=3.5.2,<4", "autobahn>=22.4.2", "twisted[tls]>=22.4"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
tests = [
|
||||
"django",
|
||||
"hypothesis",
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
"pytest-cov",
|
||||
"black",
|
||||
"tox",
|
||||
"flake8",
|
||||
"flake8-bugbear",
|
||||
"mypy",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/django/daphne"
|
||||
documentation = "https://channels.readthedocs.io"
|
||||
repository = "https://github.com/django/daphne.git"
|
||||
changelog = "https://github.com/django/daphne/blob/main/CHANGELOG.txt"
|
||||
issues = "https://github.com/django/daphne/issues"
|
||||
|
||||
[project.scripts]
|
||||
daphne = "daphne.cli:CommandLineInterface.entrypoint"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["daphne"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = { attr = "daphne.__version__" }
|
||||
readme = { file = "README.rst", content-type = "text/x-rst" }
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.pytest]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "strict"
|
||||
filterwarnings = ["ignore::pytest.PytestDeprecationWarning"]
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = ["tests/*"]
|
||||
concurrency = ["multiprocessing"]
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = "true"
|
||||
skip_covered = "true"
|
||||
|
||||
[tool.coverage.html]
|
||||
directory = "reports/coverage_html_report"
|
11
setup.cfg
11
setup.cfg
|
@ -1,11 +0,0 @@
|
|||
[flake8]
|
||||
exclude = venv/*,tox/*,docs/*,testproject/*,js_client/*,.eggs/*
|
||||
extend-ignore = E123, E128, E266, E402, W503, E731, W601
|
||||
max-line-length = 120
|
||||
|
||||
[isort]
|
||||
profile = black
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
asyncio_mode = strict
|
46
setup.py
46
setup.py
|
@ -1,46 +0,0 @@
|
|||
import os
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
from daphne import __version__
|
||||
|
||||
# We use the README as the long_description
|
||||
readme_path = os.path.join(os.path.dirname(__file__), "README.rst")
|
||||
with open(readme_path) as fp:
|
||||
long_description = fp.read()
|
||||
|
||||
setup(
|
||||
name="daphne",
|
||||
version=__version__,
|
||||
url="https://github.com/django/daphne",
|
||||
author="Django Software Foundation",
|
||||
author_email="foundation@djangoproject.com",
|
||||
description="Django ASGI (HTTP/WebSocket) server",
|
||||
long_description=long_description,
|
||||
license="BSD",
|
||||
zip_safe=False,
|
||||
package_dir={"twisted": "daphne/twisted"},
|
||||
packages=find_packages() + ["twisted.plugins"],
|
||||
include_package_data=True,
|
||||
install_requires=["twisted[tls]>=22.4", "autobahn>=22.4.2", "asgiref>=3.5.2,<4"],
|
||||
python_requires=">=3.7",
|
||||
setup_requires=["pytest-runner"],
|
||||
extras_require={"tests": ["hypothesis", "pytest", "pytest-asyncio", "django"]},
|
||||
entry_points={
|
||||
"console_scripts": ["daphne = daphne.cli:CommandLineInterface.entrypoint"]
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
],
|
||||
)
|
|
@ -304,9 +304,21 @@ class TestHTTPRequest(DaphneTestCase):
|
|||
response = self.run_daphne_raw(
|
||||
b"GET /\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
||||
)
|
||||
self.assertTrue(response.startswith(b"HTTP/1.0 400 Bad Request"))
|
||||
self.assertTrue(b"400 Bad Request" in response)
|
||||
# Bad querystring
|
||||
response = self.run_daphne_raw(
|
||||
b"GET /?\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
||||
)
|
||||
self.assertTrue(response.startswith(b"HTTP/1.0 400 Bad Request"))
|
||||
self.assertTrue(b"400 Bad Request" in response)
|
||||
|
||||
def test_invalid_header_name(self):
|
||||
"""
|
||||
Tests that requests with invalid header names fail.
|
||||
"""
|
||||
# Test cases follow those used by h11
|
||||
# https://github.com/python-hyper/h11/blob/a2c68948accadc3876dffcf979d98002e4a4ed27/h11/tests/test_headers.py#L24-L35
|
||||
for header_name in [b"foo bar", b"foo\x00bar", b"foo\xffbar", b"foo\x01bar"]:
|
||||
response = self.run_daphne_raw(
|
||||
f"GET / HTTP/1.0\r\n{header_name}: baz\r\n\r\n".encode("ascii")
|
||||
)
|
||||
self.assertTrue(b"400 Bad Request" in response)
|
||||
|
|
|
@ -192,6 +192,30 @@ class TestWebsocket(DaphneTestCase):
|
|||
self.assertEqual(scope["path"], "/foo/bar")
|
||||
self.assertEqual(scope["raw_path"], b"/foo%2Fbar")
|
||||
|
||||
@given(daphne_path=http_strategies.http_path())
|
||||
@settings(max_examples=5, deadline=2000)
|
||||
def test_root_path(self, *, daphne_path):
|
||||
"""
|
||||
Tests root_path handling.
|
||||
"""
|
||||
headers = [("Daphne-Root-Path", parse.quote(daphne_path))]
|
||||
with DaphneTestingInstance() as test_app:
|
||||
test_app.add_send_messages([{"type": "websocket.accept"}])
|
||||
self.websocket_handshake(
|
||||
test_app,
|
||||
path="/",
|
||||
headers=headers,
|
||||
)
|
||||
# Validate the scope and messages we got
|
||||
scope, _ = test_app.get_received()
|
||||
|
||||
# Daphne-Root-Path is not included in the returned 'headers' section.
|
||||
self.assertNotIn(
|
||||
"daphne-root-path", (header[0].lower() for header in scope["headers"])
|
||||
)
|
||||
# And what we're looking for, root_path being set.
|
||||
self.assertEqual(scope["root_path"], daphne_path)
|
||||
|
||||
def test_text_frames(self):
|
||||
"""
|
||||
Tests we can send and receive text frames.
|
||||
|
|
Loading…
Reference in New Issue
Block a user