Skip to content

Commit

Permalink
Clamped WsgiToAsgi response body using Content-Length value
Browse files Browse the repository at this point in the history
This makes it adhere correctly to this part of the WSGI spec. Fixes #195.
  • Loading branch information
kmichel committed Sep 16, 2020
1 parent cfd82e4 commit 03b0dbb
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 0 deletions.
17 changes: 17 additions & 0 deletions asgiref/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class WsgiToAsgiInstance:
def __init__(self, wsgi_application):
self.wsgi_application = wsgi_application
self.response_started = False
self.response_content_length = None

async def __call__(self, scope, receive, send):
if scope["type"] != "http":
Expand Down Expand Up @@ -114,6 +115,11 @@ def start_response(self, status, response_headers, exc_info=None):
(name.lower().encode("ascii"), value.encode("ascii"))
for name, value in response_headers
]
# Extract content-length
self.response_content_length = None
for name, value in response_headers:
if name.lower() == "content-length":
self.response_content_length = int(value)
# Build and send response start message.
self.response_start = {
"type": "http.response.start",
Expand All @@ -130,14 +136,25 @@ def run_wsgi_app(self, body):
# Translate the scope and incoming request body into a WSGI environ
environ = self.build_environ(self.scope, body)
# Run the WSGI app
bytes_sent = 0
for output in self.wsgi_application(environ, self.start_response):
# If this is the first response, include the response headers
if not self.response_started:
self.response_started = True
self.sync_send(self.response_start)
# If the application supplies a Content-Length header
if self.response_content_length is not None:
# The server should not transmit more bytes to the client than the header allows
bytes_allowed = self.response_content_length - bytes_sent
if len(output) > bytes_allowed:
output = output[:bytes_allowed]
self.sync_send(
{"type": "http.response.body", "body": output, "more_body": True}
)
bytes_sent += len(output)
# The server should stop iterating over the response when enough data has been sent
if bytes_sent == self.response_content_length:
break
# Close connection
if not self.response_started:
self.response_started = True
Expand Down
126 changes: 126 additions & 0 deletions tests/test_wsgi.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

import pytest

from asgiref.testing import ApplicationCommunicator
Expand Down Expand Up @@ -126,6 +128,130 @@ def wsgi_application(environ, start_response):
assert (await instance.receive_output(1)) == {"type": "http.response.body"}


@pytest.mark.asyncio
async def test_wsgi_clamped_body():
"""
Makes sure WsgiToAsgi clamps a body response longer than Content-Length
"""

def wsgi_application(environ, start_response):
start_response("200 OK", [("Content-Length", "8")])
return [b"0123", b"45", b"6789"]

application = WsgiToAsgi(wsgi_application)
instance = ApplicationCommunicator(
application,
{
"type": "http",
"http_version": "1.0",
"method": "GET",
"path": "/",
"query_string": b"",
"headers": [],
},
)
await instance.send_input({"type": "http.request"})
assert (await instance.receive_output(1)) == {
"type": "http.response.start",
"status": 200,
"headers": [(b"content-length", b"8")],
}
assert (await instance.receive_output(1)) == {
"type": "http.response.body",
"body": b"0123",
"more_body": True,
}
assert (await instance.receive_output(1)) == {
"type": "http.response.body",
"body": b"45",
"more_body": True,
}
assert (await instance.receive_output(1)) == {
"type": "http.response.body",
"body": b"67",
"more_body": True,
}
assert (await instance.receive_output(1)) == {"type": "http.response.body"}


@pytest.mark.asyncio
async def test_wsgi_stops_iterating_after_content_length_bytes():
"""
Makes sure WsgiToAsgi does not iterate after than Content-Length bytes
"""

def wsgi_application(environ, start_response):
start_response("200 OK", [("Content-Length", "4")])
yield b"0123"
pytest.fail("WsgiToAsgi should not iterate after Content-Length bytes")
yield b"4567"

application = WsgiToAsgi(wsgi_application)
instance = ApplicationCommunicator(
application,
{
"type": "http",
"http_version": "1.0",
"method": "GET",
"path": "/",
"query_string": b"",
"headers": [],
},
)
await instance.send_input({"type": "http.request"})
assert (await instance.receive_output(1)) == {
"type": "http.response.start",
"status": 200,
"headers": [(b"content-length", b"4")],
}
assert (await instance.receive_output(1)) == {
"type": "http.response.body",
"body": b"0123",
"more_body": True,
}
assert (await instance.receive_output(1)) == {"type": "http.response.body"}


@pytest.mark.asyncio
async def test_wsgi_multiple_start_response():
"""
Makes sure WsgiToAsgi only keep Content-Length from the last call to start_response
"""

def wsgi_application(environ, start_response):
start_response("200 OK", [("Content-Length", "5")])
try:
raise ValueError("Application Error")
except ValueError:
start_response("500 Server Error", [], sys.exc_info())
return [b"Some long error message"]

application = WsgiToAsgi(wsgi_application)
instance = ApplicationCommunicator(
application,
{
"type": "http",
"http_version": "1.0",
"method": "GET",
"path": "/",
"query_string": b"",
"headers": [],
},
)
await instance.send_input({"type": "http.request"})
assert (await instance.receive_output(1)) == {
"type": "http.response.start",
"status": 500,
"headers": [],
}
assert (await instance.receive_output(1)) == {
"type": "http.response.body",
"body": b"Some long error message",
"more_body": True,
}
assert (await instance.receive_output(1)) == {"type": "http.response.body"}


@pytest.mark.asyncio
async def test_wsgi_multi_body():
"""
Expand Down

0 comments on commit 03b0dbb

Please sign in to comment.