Skip to content

Commit

Permalink
Fix issue with local mode bodyless responses causing clients to hang
Browse files Browse the repository at this point in the history
fixes aws#525
  • Loading branch information
stealthycoin committed Sep 18, 2017
1 parent d5fdcbc commit e3b74f4
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 1 deletion.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
CHANGELOG
=========

Next Release (TBD)
==================
* Fix issue with chalice local mode causing http clients to hang on responses
with no body
(`#525 <https://github.com/aws/chalice/issues/525>`__)


1.0.2
=====

Expand Down
3 changes: 2 additions & 1 deletion chalice/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ class NotAuthorizedError(LocalGatewayException):


class NoOptionsRouteDefined(LocalGatewayException):
CODE = 403
CODE = 200


class LambdaContext(object):
Expand Down Expand Up @@ -574,6 +574,7 @@ def _send_http_response_with_body(self, code, headers, body):

def _send_http_response_no_body(self, code, headers):
# type: (int, HeaderType) -> None
headers['Content-Length'] = '0'
self.send_response(code)
for k, v in headers.items():
self.send_header(k, v)
Expand Down
93 changes: 93 additions & 0 deletions tests/integration/test_local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import socket
import contextlib
from threading import Thread
import pytest
import requests
from chalice import app
from chalice.local import LocalDevServer
from chalice.config import Config


class ThreadedLocalServer(Thread):
def __init__(self, port):
super(ThreadedLocalServer, self).__init__()
self._app_object = None
self._config = None
self._port = port
self._server = None

def configure(self, app_object, config):
self._app_object = app_object
self._config = config

def run(self):
self._server = LocalDevServer(
self._app_object, self._config, self._port)
self._server.serve_forever()

def shutdown(self):
if self._server is not None:
self._server.server.shutdown()


@pytest.fixture
def config():
return Config()


@pytest.fixture()
def unused_tcp_port():
with contextlib.closing(socket.socket()) as sock:
sock.bind(('127.0.0.1', 0))
return sock.getsockname()[1]


@pytest.fixture()
def local_server_factory(unused_tcp_port):
threaded_server = ThreadedLocalServer(unused_tcp_port)

def create_server(app_object, config):
threaded_server.configure(app_object, config)
threaded_server.start()
return threaded_server, unused_tcp_port

try:
yield create_server
finally:
threaded_server.shutdown()


@pytest.fixture
def sample_app():
demo = app.Chalice('demo-app')

@demo.route('/', methods=['GET'])
def index():
return {'hello': 'world'}

@demo.route('/test-cors', methods=['POST'], cors=True)
def test_cors():
return {'hello': 'world'}

return demo


def _call_local_server(method, path, port, timeout=0.5):
return method('http://localhost:{port}{path}'.format(
path=path, port=port), timeout=timeout)


def test_can_accept_get_request(config, sample_app, local_server_factory):
local_server, port = local_server_factory(sample_app, config)
response = _call_local_server(requests.get, '/', port)
assert response.status_code == 200
assert response.text == '{"hello": "world"}'


def test_can_accept_options_request(config, sample_app, local_server_factory):
local_server, port = local_server_factory(sample_app, config)
response = _call_local_server(requests.options, '/test-cors', port)
assert response.status_code == 200
assert response.headers['Content-Length'] == '0'
assert response.headers['Access-Control-Allow-Methods'] == 'POST,OPTIONS'
assert response.text == ''
3 changes: 3 additions & 0 deletions tests/unit/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ def test_will_respond_with_custom_cors_enabled(handler):
headers=headers)
handler.do_GET()
response = handler.wfile.getvalue().splitlines()
assert b'HTTP/1.1 200 OK' in response
assert b'Access-Control-Allow-Origin: https://foo.bar' in response
assert (b'Access-Control-Allow-Headers: Authorization,Content-Type,'
b'Header-A,Header-B,X-Amz-Date,X-Amz-Security-Token,'
Expand All @@ -360,13 +361,15 @@ def test_will_respond_with_custom_cors_enabled_options(handler):
headers=headers)
handler.do_OPTIONS()
response = handler.wfile.getvalue().decode().splitlines()
assert 'HTTP/1.1 200 OK' in response
assert 'Access-Control-Allow-Origin: https://foo.bar' in response
assert ('Access-Control-Allow-Headers: Authorization,Content-Type,'
'Header-A,Header-B,X-Amz-Date,X-Amz-Security-Token,'
'X-Api-Key') in response
assert 'Access-Control-Expose-Headers: Header-A,Header-B' in response
assert 'Access-Control-Max-Age: 600' in response
assert 'Access-Control-Allow-Credentials: true' in response
assert 'Content-Length: 0' in response

# Ensure that the Access-Control-Allow-Methods header is sent
# and that it sends all the correct methods over.
Expand Down

0 comments on commit e3b74f4

Please sign in to comment.