Merge pull request #378 from Pylons/bugfix/expose_tracebacks-encode-error

Bugfix: expose_tracebacks encode error
diff --git a/src/waitress/task.py b/src/waitress/task.py
index a003919..3bc7f7c 100644
--- a/src/waitress/task.py
+++ b/src/waitress/task.py
@@ -355,7 +355,7 @@
         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):
diff --git a/src/waitress/utilities.py b/src/waitress/utilities.py
index 6ae4742..a9d3361 100644
--- a/src/waitress/utilities.py
+++ b/src/waitress/utilities.py
@@ -262,8 +262,8 @@
         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/fixtureapps/error_traceback.py b/tests/fixtureapps/error_traceback.py
new file mode 100644
index 0000000..24e4cbf
--- /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 60eb24a..1dfd889 100644
--- a/tests/test_functional.py
+++ b/tests/test_functional.py
@@ -359,7 +359,7 @@
                 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 @@
                 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 @@
                 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 @@
                 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 @@
             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)
@@ -1269,6 +1269,49 @@
             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 @@
     pass
 
 
+class TcpInternalServerErrorTestsWithTraceback(
+    InternalServerErrorTestsWithTraceback, TcpTests, unittest.TestCase
+):
+    pass
+
+
 class TcpFileWrapperTests(FileWrapperTests, TcpTests, unittest.TestCase):
     pass
 
@@ -1604,6 +1653,11 @@
     ):
         pass
 
+    class UnixInternalServerErrorTestsWithTraceback(
+        InternalServerErrorTestsWithTraceback, UnixTests, unittest.TestCase
+    ):
+        pass
+
     class UnixFileWrapperTests(FileWrapperTests, UnixTests, unittest.TestCase):
         pass
 
diff --git a/tests/test_proxy_headers.py b/tests/test_proxy_headers.py
index 9ed131e..45f9878 100644
--- a/tests/test_proxy_headers.py
+++ b/tests/test_proxy_headers.py
@@ -16,7 +16,7 @@
             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 @@
     def __call__(self, environ, start_response):
         self.environ = environ
         start_response("200 OK", [("Content-Type", "text/plain")])
-        yield "hello"
+        yield b"hello"
 
 
 class DummyResponse:
diff --git a/tests/test_task.py b/tests/test_task.py
index cc579b0..47868e1 100644
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -869,7 +869,7 @@
         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 @@
         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 @@
         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 @@
         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")