mirror of
https://github.com/django/daphne.git
synced 2025-06-28 17:03:03 +03:00
Add test suite for ASGI handlers
This commit is contained in:
parent
17e9824f71
commit
5cd2cbdfee
|
@ -9,6 +9,7 @@ from django.core.handlers import base
|
||||||
from django.core import signals
|
from django.core import signals
|
||||||
from django.core.urlresolvers import set_script_prefix
|
from django.core.urlresolvers import set_script_prefix
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
logger = logging.getLogger('django.request')
|
logger = logging.getLogger('django.request')
|
||||||
|
|
||||||
|
@ -21,8 +22,10 @@ class AsgiRequest(http.HttpRequest):
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
self.message = message
|
self.message = message
|
||||||
self.reply_channel = self.message['reply_channel']
|
self.reply_channel = self.message.reply_channel
|
||||||
self._content_length = 0
|
self._content_length = 0
|
||||||
|
self._post_parse_error = False
|
||||||
|
self.resolver_match = None
|
||||||
# Path info
|
# Path info
|
||||||
self.path = self.message['path']
|
self.path = self.message['path']
|
||||||
self.script_name = self.message.get('root_path', '')
|
self.script_name = self.message.get('root_path', '')
|
||||||
|
@ -63,7 +66,7 @@ class AsgiRequest(http.HttpRequest):
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
# Body handling
|
# Body handling
|
||||||
self._body = message.get("body", "")
|
self._body = message.get("body", b"")
|
||||||
if message.get("body_channel", None):
|
if message.get("body_channel", None):
|
||||||
while True:
|
while True:
|
||||||
# Get the next chunk from the request body channel
|
# Get the next chunk from the request body channel
|
||||||
|
@ -78,6 +81,7 @@ class AsgiRequest(http.HttpRequest):
|
||||||
# Exit loop if this was the last
|
# Exit loop if this was the last
|
||||||
if not chunk.get("more_content", False):
|
if not chunk.get("more_content", False):
|
||||||
break
|
break
|
||||||
|
assert isinstance(self._body, six.binary_type), "Body is not bytes"
|
||||||
# Other bits
|
# Other bits
|
||||||
self.resolver_match = None
|
self.resolver_match = None
|
||||||
|
|
||||||
|
@ -97,7 +101,13 @@ class AsgiRequest(http.HttpRequest):
|
||||||
def _set_post(self, post):
|
def _set_post(self, post):
|
||||||
self._post = post
|
self._post = post
|
||||||
|
|
||||||
|
def _get_files(self):
|
||||||
|
if not hasattr(self, '_files'):
|
||||||
|
self._load_post_and_files()
|
||||||
|
return self._files
|
||||||
|
|
||||||
POST = property(_get_post, _set_post)
|
POST = property(_get_post, _set_post)
|
||||||
|
FILES = property(_get_files)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def COOKIES(self):
|
def COOKIES(self):
|
||||||
|
@ -114,6 +124,9 @@ class AsgiHandler(base.BaseHandler):
|
||||||
initLock = Lock()
|
initLock = Lock()
|
||||||
request_class = AsgiRequest
|
request_class = AsgiRequest
|
||||||
|
|
||||||
|
# Size to chunk response bodies into for multiple response messages
|
||||||
|
chunk_size = 512 * 1024
|
||||||
|
|
||||||
def __call__(self, message):
|
def __call__(self, message):
|
||||||
# Set up middleware if needed. We couldn't do this earlier, because
|
# Set up middleware if needed. We couldn't do this earlier, because
|
||||||
# settings weren't available.
|
# settings weren't available.
|
||||||
|
@ -148,7 +161,9 @@ class AsgiHandler(base.BaseHandler):
|
||||||
"""
|
"""
|
||||||
Encodes a Django HTTP response into an ASGI http.response message(s).
|
Encodes a Django HTTP response into an ASGI http.response message(s).
|
||||||
"""
|
"""
|
||||||
# Collect cookies into headers
|
# Collect cookies into headers.
|
||||||
|
# Note that we have to preserve header case as there are some non-RFC
|
||||||
|
# compliant clients that want things like Content-Type correct. Ugh.
|
||||||
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
||||||
for c in response.cookies.values():
|
for c in response.cookies.values():
|
||||||
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
||||||
|
@ -186,14 +201,13 @@ class AsgiHandler(base.BaseHandler):
|
||||||
|
|
||||||
Yields (chunk, last_chunk) tuples.
|
Yields (chunk, last_chunk) tuples.
|
||||||
"""
|
"""
|
||||||
CHUNK_SIZE = 512 * 1024
|
|
||||||
position = 0
|
position = 0
|
||||||
while position < len(data):
|
while position < len(data):
|
||||||
yield (
|
yield (
|
||||||
data[position:position+CHUNK_SIZE],
|
data[position:position + self.chunk_size],
|
||||||
(position + CHUNK_SIZE) >= len(data),
|
(position + self.chunk_size) >= len(data),
|
||||||
)
|
)
|
||||||
position += CHUNK_SIZE
|
position += self.chunk_size
|
||||||
|
|
||||||
|
|
||||||
class ViewConsumer(object):
|
class ViewConsumer(object):
|
||||||
|
@ -205,5 +219,5 @@ class ViewConsumer(object):
|
||||||
self.handler = AsgiHandler()
|
self.handler = AsgiHandler()
|
||||||
|
|
||||||
def __call__(self, message):
|
def __call__(self, message):
|
||||||
for reply_message in self.handler(message.content):
|
for reply_message in self.handler(message):
|
||||||
message.reply_channel.send(reply_message)
|
message.reply_channel.send(reply_message)
|
||||||
|
|
|
@ -12,16 +12,20 @@ class Message(object):
|
||||||
to use to reply to this message's end user, if that makes sense.
|
to use to reply to this message's end user, if that makes sense.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Requeue(Exception):
|
def __init__(self, content, channel, channel_layer):
|
||||||
"""
|
|
||||||
Raise this while processing a message to requeue it back onto the
|
|
||||||
channel. Useful if you're manually ensuring partial ordering, etc.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __init__(self, content, channel, channel_layer, reply_channel=None):
|
|
||||||
self.content = content
|
self.content = content
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.channel_layer = channel_layer
|
self.channel_layer = channel_layer
|
||||||
if reply_channel:
|
if content.get("reply_channel", None):
|
||||||
self.reply_channel = Channel(reply_channel, channel_layer=self.channel_layer)
|
self.reply_channel = Channel(
|
||||||
|
content["reply_channel"],
|
||||||
|
channel_layer=self.channel_layer,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.reply_channel = None
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.content[key]
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return self.content.get(key, default)
|
||||||
|
|
90
channels/tests/test_handler.py
Normal file
90
channels/tests/test_handler.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from asgiref.inmemory import ChannelLayer
|
||||||
|
from channels.handler import AsgiHandler
|
||||||
|
from channels.message import Message
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAsgiHandler(AsgiHandler):
|
||||||
|
"""
|
||||||
|
Handler subclass that just returns a premade response rather than
|
||||||
|
go into the view subsystem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
chunk_size = 30
|
||||||
|
|
||||||
|
def __init__(self, response):
|
||||||
|
assert isinstance(response, HttpResponse)
|
||||||
|
self._response = response
|
||||||
|
super(FakeAsgiHandler, self).__init__()
|
||||||
|
|
||||||
|
def get_response(self, request):
|
||||||
|
return self._response
|
||||||
|
|
||||||
|
|
||||||
|
class HandlerTests(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Tests that the handler works correctly and round-trips things into a
|
||||||
|
correct response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Make an in memory channel layer for testing
|
||||||
|
"""
|
||||||
|
self.channel_layer = ChannelLayer()
|
||||||
|
self.make_message = lambda m, c: Message(m, c, self.channel_layer)
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
"""
|
||||||
|
Tests a simple request
|
||||||
|
"""
|
||||||
|
# Make stub request and desired response
|
||||||
|
message = self.make_message({
|
||||||
|
"reply_channel": "test",
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/test/",
|
||||||
|
}, "test")
|
||||||
|
response = HttpResponse(b"Hi there!", content_type="text/plain")
|
||||||
|
# Run the handler
|
||||||
|
handler = FakeAsgiHandler(response)
|
||||||
|
reply_messages = list(handler(message))
|
||||||
|
# Make sure we got the right number of messages
|
||||||
|
self.assertEqual(len(reply_messages), 1)
|
||||||
|
reply_message = reply_messages[0]
|
||||||
|
# Make sure the message looks correct
|
||||||
|
self.assertEqual(reply_message["content"], b"Hi there!")
|
||||||
|
self.assertEqual(reply_message["status"], 200)
|
||||||
|
self.assertEqual(reply_message["status_text"], "OK")
|
||||||
|
self.assertEqual(reply_message.get("more_content", False), False)
|
||||||
|
self.assertEqual(
|
||||||
|
reply_message["headers"],
|
||||||
|
[("Content-Type", b"text/plain")],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_large(self):
|
||||||
|
"""
|
||||||
|
Tests a large response (will need chunking)
|
||||||
|
"""
|
||||||
|
# Make stub request and desired response
|
||||||
|
message = self.make_message({
|
||||||
|
"reply_channel": "test",
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/test/",
|
||||||
|
}, "test")
|
||||||
|
response = HttpResponse(b"Thefirstthirtybytesisrighthereandhereistherest")
|
||||||
|
# Run the handler
|
||||||
|
handler = FakeAsgiHandler(response)
|
||||||
|
reply_messages = list(handler(message))
|
||||||
|
# Make sure we got the right number of messages
|
||||||
|
self.assertEqual(len(reply_messages), 2)
|
||||||
|
# Make sure the messages look correct
|
||||||
|
self.assertEqual(reply_messages[0]["content"], b"Thefirstthirtybytesisrighthere")
|
||||||
|
self.assertEqual(reply_messages[0]["status"], 200)
|
||||||
|
self.assertEqual(reply_messages[0]["more_content"], True)
|
||||||
|
self.assertEqual(reply_messages[1]["content"], b"andhereistherest")
|
||||||
|
self.assertEqual(reply_messages[1].get("more_content", False), False)
|
178
channels/tests/test_request.py
Normal file
178
channels/tests/test_request.py
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from asgiref.inmemory import ChannelLayer
|
||||||
|
from channels.handler import AsgiRequest
|
||||||
|
from channels.message import Message
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTests(SimpleTestCase):
|
||||||
|
"""
|
||||||
|
Tests that ASGI request handling correctly decodes HTTP requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Make an in memory channel layer for testing
|
||||||
|
"""
|
||||||
|
self.channel_layer = ChannelLayer()
|
||||||
|
self.make_message = lambda m, c: Message(m, c, self.channel_layer)
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
"""
|
||||||
|
Tests that the handler can decode the most basic request message,
|
||||||
|
with all optional fields omitted.
|
||||||
|
"""
|
||||||
|
message = self.make_message({
|
||||||
|
"reply_channel": "test-reply",
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/test/",
|
||||||
|
}, "test")
|
||||||
|
request = AsgiRequest(message)
|
||||||
|
self.assertEqual(request.path, "/test/")
|
||||||
|
self.assertEqual(request.method, "GET")
|
||||||
|
self.assertFalse(request.body)
|
||||||
|
self.assertNotIn("HTTP_HOST", request.META)
|
||||||
|
self.assertNotIn("REMOTE_ADDR", request.META)
|
||||||
|
self.assertNotIn("REMOTE_HOST", request.META)
|
||||||
|
self.assertNotIn("REMOTE_PORT", request.META)
|
||||||
|
self.assertNotIn("SERVER_NAME", request.META)
|
||||||
|
self.assertNotIn("SERVER_PORT", request.META)
|
||||||
|
self.assertFalse(request.GET)
|
||||||
|
self.assertFalse(request.POST)
|
||||||
|
self.assertFalse(request.COOKIES)
|
||||||
|
|
||||||
|
def test_extended(self):
|
||||||
|
"""
|
||||||
|
Tests a more fully-featured GET request
|
||||||
|
"""
|
||||||
|
message = self.make_message({
|
||||||
|
"reply_channel": "test",
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/test2/",
|
||||||
|
"query_string": b"x=1&y=foo%20bar+baz",
|
||||||
|
"headers": {
|
||||||
|
"host": b"example.com",
|
||||||
|
"cookie": b"test-time=1448995585123; test-value=yeah",
|
||||||
|
},
|
||||||
|
"client": ["10.0.0.1", 1234],
|
||||||
|
"server": ["10.0.0.2", 80],
|
||||||
|
}, "test")
|
||||||
|
request = AsgiRequest(message)
|
||||||
|
self.assertEqual(request.path, "/test2/")
|
||||||
|
self.assertEqual(request.method, "GET")
|
||||||
|
self.assertFalse(request.body)
|
||||||
|
self.assertEqual(request.META["HTTP_HOST"], "example.com")
|
||||||
|
self.assertEqual(request.META["REMOTE_ADDR"], "10.0.0.1")
|
||||||
|
self.assertEqual(request.META["REMOTE_HOST"], "10.0.0.1")
|
||||||
|
self.assertEqual(request.META["REMOTE_PORT"], 1234)
|
||||||
|
self.assertEqual(request.META["SERVER_NAME"], "10.0.0.2")
|
||||||
|
self.assertEqual(request.META["SERVER_PORT"], 80)
|
||||||
|
self.assertEqual(request.GET["x"], "1")
|
||||||
|
self.assertEqual(request.GET["y"], "foo bar baz")
|
||||||
|
self.assertEqual(request.COOKIES["test-time"], "1448995585123")
|
||||||
|
self.assertEqual(request.COOKIES["test-value"], "yeah")
|
||||||
|
self.assertFalse(request.POST)
|
||||||
|
|
||||||
|
def test_post_single(self):
|
||||||
|
"""
|
||||||
|
Tests a POST body contained within a single message.
|
||||||
|
"""
|
||||||
|
message = self.make_message({
|
||||||
|
"reply_channel": "test",
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/test2/",
|
||||||
|
"query_string": b"django=great",
|
||||||
|
"body": b"ponies=are+awesome",
|
||||||
|
"headers": {
|
||||||
|
"host": b"example.com",
|
||||||
|
"content-type": b"application/x-www-form-urlencoded",
|
||||||
|
"content-length": b"18",
|
||||||
|
},
|
||||||
|
}, "test")
|
||||||
|
request = AsgiRequest(message)
|
||||||
|
self.assertEqual(request.path, "/test2/")
|
||||||
|
self.assertEqual(request.method, "POST")
|
||||||
|
self.assertEqual(request.body, b"ponies=are+awesome")
|
||||||
|
self.assertEqual(request.META["HTTP_HOST"], "example.com")
|
||||||
|
self.assertEqual(request.META["CONTENT_TYPE"], "application/x-www-form-urlencoded")
|
||||||
|
self.assertEqual(request.GET["django"], "great")
|
||||||
|
self.assertEqual(request.POST["ponies"], "are awesome")
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
request.POST["django"]
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
request.GET["ponies"]
|
||||||
|
|
||||||
|
def test_post_multiple(self):
|
||||||
|
"""
|
||||||
|
Tests a POST body across multiple messages (first part in 'body').
|
||||||
|
"""
|
||||||
|
message = self.make_message({
|
||||||
|
"reply_channel": "test",
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/test/",
|
||||||
|
"body": b"there_a",
|
||||||
|
"body_channel": "test-input",
|
||||||
|
"headers": {
|
||||||
|
"host": b"example.com",
|
||||||
|
"content-type": b"application/x-www-form-urlencoded",
|
||||||
|
"content-length": b"21",
|
||||||
|
},
|
||||||
|
}, "test")
|
||||||
|
self.channel_layer.send("test-input", {
|
||||||
|
"content": b"re=fou",
|
||||||
|
"more_content": True,
|
||||||
|
})
|
||||||
|
self.channel_layer.send("test-input", {
|
||||||
|
"content": b"r+lights",
|
||||||
|
})
|
||||||
|
request = AsgiRequest(message)
|
||||||
|
self.assertEqual(request.method, "POST")
|
||||||
|
self.assertEqual(request.body, b"there_are=four+lights")
|
||||||
|
self.assertEqual(request.META["CONTENT_TYPE"], "application/x-www-form-urlencoded")
|
||||||
|
self.assertEqual(request.POST["there_are"], "four lights")
|
||||||
|
|
||||||
|
def test_post_files(self):
|
||||||
|
"""
|
||||||
|
Tests POSTing files using multipart form data and multiple messages,
|
||||||
|
with no body in the initial message.
|
||||||
|
"""
|
||||||
|
body = (
|
||||||
|
b'--BOUNDARY\r\n' +
|
||||||
|
b'Content-Disposition: form-data; name="title"\r\n\r\n' +
|
||||||
|
b'My First Book\r\n' +
|
||||||
|
b'--BOUNDARY\r\n' +
|
||||||
|
b'Content-Disposition: form-data; name="pdf"; filename="book.pdf"\r\n\r\n' +
|
||||||
|
b'FAKEPDFBYTESGOHERE' +
|
||||||
|
b'--BOUNDARY--'
|
||||||
|
)
|
||||||
|
message = self.make_message({
|
||||||
|
"reply_channel": "test",
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/test/",
|
||||||
|
"body_channel": "test-input",
|
||||||
|
"headers": {
|
||||||
|
"content-type": b"multipart/form-data; boundary=BOUNDARY",
|
||||||
|
"content-length": six.binary_type(len(body)),
|
||||||
|
},
|
||||||
|
}, "test")
|
||||||
|
self.channel_layer.send("test-input", {
|
||||||
|
"content": body[:20],
|
||||||
|
"more_content": True,
|
||||||
|
})
|
||||||
|
self.channel_layer.send("test-input", {
|
||||||
|
"content": body[20:],
|
||||||
|
})
|
||||||
|
request = AsgiRequest(message)
|
||||||
|
self.assertEqual(request.method, "POST")
|
||||||
|
self.assertEqual(len(request.body), len(body))
|
||||||
|
self.assertTrue(request.META["CONTENT_TYPE"].startswith("multipart/form-data"))
|
||||||
|
self.assertFalse(request._post_parse_error)
|
||||||
|
self.assertEqual(request.POST["title"], "My First Book")
|
||||||
|
self.assertEqual(request.FILES["pdf"].read(), "FAKEPDFBYTESGOHERE")
|
|
@ -35,7 +35,6 @@ class Worker(object):
|
||||||
content=content,
|
content=content,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
channel_layer=self.channel_layer,
|
channel_layer=self.channel_layer,
|
||||||
reply_channel=content.get("reply_channel", None),
|
|
||||||
)
|
)
|
||||||
# Handle the message
|
# Handle the message
|
||||||
consumer = self.channel_layer.registry.consumer_for_channel(channel)
|
consumer = self.channel_layer.registry.consumer_for_channel(channel)
|
||||||
|
|
|
@ -431,30 +431,30 @@ Keys:
|
||||||
* ``method``: Unicode string HTTP method name, uppercased.
|
* ``method``: Unicode string HTTP method name, uppercased.
|
||||||
|
|
||||||
* ``scheme``: Unicode string URL scheme portion (likely ``http`` or ``https``).
|
* ``scheme``: Unicode string URL scheme portion (likely ``http`` or ``https``).
|
||||||
Optional (but must not be empty), default is ``http``.
|
Optional (but must not be empty), default is ``"http"``.
|
||||||
|
|
||||||
* ``path``: Byte string HTTP path from URL.
|
* ``path``: Byte string HTTP path from URL.
|
||||||
|
|
||||||
* ``query_string``: Byte string URL portion after the ``?``. Optional, default
|
* ``query_string``: Byte string URL portion after the ``?``. Optional, default
|
||||||
is empty string.
|
is ``""``.
|
||||||
|
|
||||||
* ``root_path``: Byte string that indicates the root path this application
|
* ``root_path``: Byte string that indicates the root path this application
|
||||||
is mounted at; same as ``SCRIPT_NAME`` in WSGI. Optional, defaults
|
is mounted at; same as ``SCRIPT_NAME`` in WSGI. Optional, defaults
|
||||||
to empty string.
|
to ``""``.
|
||||||
|
|
||||||
* ``headers``: Dict of ``{name: value}``, where ``name`` is the lowercased
|
* ``headers``: Dict of ``{name: value}``, where ``name`` is the lowercased
|
||||||
HTTP header name as byte string and ``value`` is the header value as a byte
|
HTTP header name as unicode string and ``value`` is the header value as a byte
|
||||||
string. If multiple headers with the same name are received, they should
|
string. If multiple headers with the same name are received, they should
|
||||||
be concatenated into a single header as per RFC 2616. Header names containing
|
be concatenated into a single header as per RFC 2616. Header names containing
|
||||||
underscores should be discarded by the server.
|
underscores should be discarded by the server. Optional, defaults to ``{}``.
|
||||||
|
|
||||||
* ``body``: Body of the request, as a byte string. Optional, defaults to empty
|
* ``body``: Body of the request, as a byte string. Optional, defaults to ``""``.
|
||||||
string. If ``body_channel`` is set, treat as start of body and concatenate
|
If ``body_channel`` is set, treat as start of body and concatenate
|
||||||
on further chunks.
|
on further chunks.
|
||||||
|
|
||||||
* ``body_channel``: Single-reader channel name that contains
|
* ``body_channel``: Single-reader channel name that contains
|
||||||
Request Body Chunk messages representing a large request body.
|
Request Body Chunk messages representing a large request body.
|
||||||
Optional, defaults to None. Chunks append to ``body`` if set. Presence of
|
Optional, defaults to ``None``. Chunks append to ``body`` if set. Presence of
|
||||||
a channel indicates at least one Request Body Chunk message needs to be read,
|
a channel indicates at least one Request Body Chunk message needs to be read,
|
||||||
and then further consumption keyed off of the ``more_content`` key in those
|
and then further consumption keyed off of the ``more_content`` key in those
|
||||||
messages.
|
messages.
|
||||||
|
@ -501,9 +501,9 @@ Keys:
|
||||||
Ignored for HTTP/2 clients. Optional, default should be based on ``status``
|
Ignored for HTTP/2 clients. Optional, default should be based on ``status``
|
||||||
or left as empty string if no default found.
|
or left as empty string if no default found.
|
||||||
|
|
||||||
* ``headers``: A list of ``[name, value]`` pairs, where ``name`` is the byte
|
* ``headers``: A list of ``[name, value]`` pairs, where ``name`` is the
|
||||||
string header name, and ``value`` is the byte string header value. Order
|
unicode string header name, and ``value`` is the byte string
|
||||||
should be preserved in the HTTP response.
|
header value. Order should be preserved in the HTTP response.
|
||||||
|
|
||||||
* ``content``: Byte string of HTTP body content.
|
* ``content``: Byte string of HTTP body content.
|
||||||
Optional, defaults to empty string.
|
Optional, defaults to empty string.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user