From 99f7135d32009cc1980db16a2aa3935a1a150277 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 21 Apr 2022 13:32:07 -0600 Subject: [PATCH 1/4] Add failing test for error_traceback --- tests/fixtureapps/error_traceback.py | 2 ++ tests/test_functional.py | 54 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/fixtureapps/error_traceback.py diff --git a/tests/fixtureapps/error_traceback.py b/tests/fixtureapps/error_traceback.py new file mode 100644 index 00000000..24e4cbf6 --- /dev/null +++ b/tests/fixtureapps/error_traceback.py @@ -0,0 +1,2 @@ +def app(environ, start_response): # pragma: no cover + raise ValueError("Invalid application: " + chr(8364)) diff --git a/tests/test_functional.py b/tests/test_functional.py index 60eb24a4..43e9d0c7 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1269,6 +1269,49 @@ def test_in_generator(self): self.assertRaises(ConnectionClosed, read_http, fp) +class InternalServerErrorTestsWithTraceback: + def setUp(self): + from tests.fixtureapps import error_traceback + + self.start_subprocess(error_traceback.app, expose_tracebacks=True) + + def tearDown(self): + self.stop_subprocess() + + def test_expose_tracebacks_http_10(self): + to_send = b"GET / HTTP/1.0\r\n\r\n" + self.connect() + self.sock.send(to_send) + with self.sock.makefile("rb", 0) as fp: + line, headers, response_body = read_http(fp) + self.assertline(line, "500", "Internal Server Error", "HTTP/1.0") + cl = int(headers["content-length"]) + self.assertEqual(cl, len(response_body)) + self.assertTrue(response_body.startswith(b"Internal Server Error")) + self.assertEqual(headers["connection"], "close") + # connection has been closed + self.send_check_error(to_send) + self.assertRaises(ConnectionClosed, read_http, fp) + + def test_expose_tracebacks_http_11(self): + to_send = b"GET / HTTP/1.1\r\n\r\n" + self.connect() + self.sock.send(to_send) + with self.sock.makefile("rb", 0) as fp: + line, headers, response_body = read_http(fp) + self.assertline(line, "500", "Internal Server Error", "HTTP/1.1") + cl = int(headers["content-length"]) + self.assertEqual(cl, len(response_body)) + self.assertTrue(response_body.startswith(b"Internal Server Error")) + self.assertEqual( + sorted(headers.keys()), + ["connection", "content-length", "content-type", "date", "server"], + ) + # connection has been closed + self.send_check_error(to_send) + self.assertRaises(ConnectionClosed, read_http, fp) + + class FileWrapperTests: def setUp(self): from tests.fixtureapps import filewrapper @@ -1538,6 +1581,12 @@ class TcpInternalServerErrorTests( pass +class TcpInternalServerErrorTestsWithTraceback( + InternalServerErrorTestsWithTraceback, TcpTests, unittest.TestCase +): + pass + + class TcpFileWrapperTests(FileWrapperTests, TcpTests, unittest.TestCase): pass @@ -1604,6 +1653,11 @@ class UnixInternalServerErrorTests( ): pass + class UnixInternalServerErrorTestsWithTraceback( + InternalServerErrorTestsWithTraceback, UnixTests, unittest.TestCase + ): + pass + class UnixFileWrapperTests(FileWrapperTests, UnixTests, unittest.TestCase): pass From 7a145701f2198dea785c080a86ed0a482d6bc4b5 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 21 Apr 2022 13:40:54 -0600 Subject: [PATCH 2/4] e.to_response() should return bytes object for body --- src/waitress/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/waitress/task.py b/src/waitress/task.py index a003919f..3bc7f7cf 100644 --- a/src/waitress/task.py +++ b/src/waitress/task.py @@ -355,7 +355,7 @@ def execute(self): self.response_headers.append(("Connection", "close")) self.close_on_finish = True self.content_length = len(body) - self.write(body.encode("latin-1")) + self.write(body) class WSGITask(Task): From b6ad056d64b3e269518f87cd33dedaeb95243ba8 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 21 Apr 2022 13:41:30 -0600 Subject: [PATCH 3/4] Always encode the responses to utf-8 and set Content-Type --- src/waitress/utilities.py | 4 ++-- tests/test_functional.py | 10 +++++----- tests/test_task.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/waitress/utilities.py b/src/waitress/utilities.py index 6ae47420..a9d33618 100644 --- a/src/waitress/utilities.py +++ b/src/waitress/utilities.py @@ -262,8 +262,8 @@ def to_response(self): status = "%s %s" % (self.code, self.reason) body = "%s\r\n\r\n%s" % (self.reason, self.body) tag = "\r\n\r\n(generated by waitress)" - body = body + tag - headers = [("Content-Type", "text/plain")] + body = (body + tag).encode("utf-8") + headers = [("Content-Type", "text/plain; charset=utf-8")] return status, headers, body diff --git a/tests/test_functional.py b/tests/test_functional.py index 43e9d0c7..1dfd8891 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -359,7 +359,7 @@ def test_broken_chunked_encoding(self): sorted(headers.keys()), ["connection", "content-length", "content-type", "date", "server"], ) - self.assertEqual(headers["content-type"], "text/plain") + self.assertEqual(headers["content-type"], "text/plain; charset=utf-8") # connection has been closed self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) @@ -381,7 +381,7 @@ def test_broken_chunked_encoding_invalid_hex(self): sorted(headers.keys()), ["connection", "content-length", "content-type", "date", "server"], ) - self.assertEqual(headers["content-type"], "text/plain") + self.assertEqual(headers["content-type"], "text/plain; charset=utf-8") # connection has been closed self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) @@ -403,7 +403,7 @@ def test_broken_chunked_encoding_invalid_extension(self): sorted(headers.keys()), ["connection", "content-length", "content-type", "date", "server"], ) - self.assertEqual(headers["content-type"], "text/plain") + self.assertEqual(headers["content-type"], "text/plain; charset=utf-8") # connection has been closed self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) @@ -428,7 +428,7 @@ def test_broken_chunked_encoding_missing_chunk_end(self): sorted(headers.keys()), ["connection", "content-length", "content-type", "date", "server"], ) - self.assertEqual(headers["content-type"], "text/plain") + self.assertEqual(headers["content-type"], "text/plain; charset=utf-8") # connection has been closed self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) @@ -1121,7 +1121,7 @@ def test_request_body_too_large_chunked_encoding(self): self.assertline(line, "413", "Request Entity Too Large", "HTTP/1.1") cl = int(headers["content-length"]) self.assertEqual(cl, len(response_body)) - self.assertEqual(headers["content-type"], "text/plain") + self.assertEqual(headers["content-type"], "text/plain; charset=utf-8") # connection has been closed self.send_check_error(to_send) self.assertRaises(ConnectionClosed, read_http, fp) diff --git a/tests/test_task.py b/tests/test_task.py index cc579b03..47868e15 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -869,7 +869,7 @@ def test_execute_http_10(self): self.assertEqual(lines[0], b"HTTP/1.0 432 Too Ugly") self.assertEqual(lines[1], b"Connection: close") self.assertEqual(lines[2], b"Content-Length: 43") - self.assertEqual(lines[3], b"Content-Type: text/plain") + self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8") self.assertTrue(lines[4]) self.assertEqual(lines[5], b"Server: waitress") self.assertEqual(lines[6], b"Too Ugly") @@ -885,7 +885,7 @@ def test_execute_http_11(self): self.assertEqual(lines[0], b"HTTP/1.1 432 Too Ugly") self.assertEqual(lines[1], b"Connection: close") self.assertEqual(lines[2], b"Content-Length: 43") - self.assertEqual(lines[3], b"Content-Type: text/plain") + self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8") self.assertTrue(lines[4]) self.assertEqual(lines[5], b"Server: waitress") self.assertEqual(lines[6], b"Too Ugly") @@ -902,7 +902,7 @@ def test_execute_http_11_close(self): self.assertEqual(lines[0], b"HTTP/1.1 432 Too Ugly") self.assertEqual(lines[1], b"Connection: close") self.assertEqual(lines[2], b"Content-Length: 43") - self.assertEqual(lines[3], b"Content-Type: text/plain") + self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8") self.assertTrue(lines[4]) self.assertEqual(lines[5], b"Server: waitress") self.assertEqual(lines[6], b"Too Ugly") @@ -919,7 +919,7 @@ def test_execute_http_11_keep_forces_close(self): self.assertEqual(lines[0], b"HTTP/1.1 432 Too Ugly") self.assertEqual(lines[1], b"Connection: close") self.assertEqual(lines[2], b"Content-Length: 43") - self.assertEqual(lines[3], b"Content-Type: text/plain") + self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8") self.assertTrue(lines[4]) self.assertEqual(lines[5], b"Server: waitress") self.assertEqual(lines[6], b"Too Ugly") From 4467d76725998d921c3e550479a88eca367bccc6 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 21 Apr 2022 13:42:03 -0600 Subject: [PATCH 4/4] Fix tests to assume body is bytes --- tests/test_proxy_headers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_proxy_headers.py b/tests/test_proxy_headers.py index 9ed131ef..45f98785 100644 --- a/tests/test_proxy_headers.py +++ b/tests/test_proxy_headers.py @@ -16,7 +16,7 @@ def start_response(status, response_headers): response.headers = response_headers response.steps = list(app(environ, start_response)) - response.body = b"".join(s.encode("latin-1") for s in response.steps) + response.body = b"".join(s for s in response.steps) return response def test_get_environment_values_w_scheme_override_untrusted(self): @@ -727,7 +727,7 @@ class DummyApp: def __call__(self, environ, start_response): self.environ = environ start_response("200 OK", [("Content-Type", "text/plain")]) - yield "hello" + yield b"hello" class DummyResponse: