diff -r 0ddba0abab49 Doc/library/http.server.rst --- a/Doc/library/http.server.rst Thu Jun 02 10:13:47 2016 +0000 +++ b/Doc/library/http.server.rst Mon Jun 06 01:14:15 2016 -0400 @@ -191,7 +191,9 @@ a complete set of headers, as the response body. The :attr:`responses` attribute holds the default values for *message* and *explain* that will be used if no value is provided; for unknown codes the default value - for both is the string ``???``. + for both is the string ``???``. The body will be empty if the method is + HEAD or the response code is one of the following: ``1xx``, + ``204 No Content``, ``205 Reset Content``, ``304 Not Modified``. .. versionchanged:: 3.4 The error response includes a Content-Length header. diff -r 0ddba0abab49 Lib/http/server.py --- a/Lib/http/server.py Thu Jun 02 10:13:47 2016 +0000 +++ b/Lib/http/server.py Mon Jun 06 01:14:15 2016 -0400 @@ -446,23 +446,30 @@ if explain is None: explain = longmsg self.log_error("code %d, message %s", code, message) - # HTML encode to prevent Cross Site Scripting attacks (see bug #1100201) - content = (self.error_message_format % { - 'code': code, - 'message': html.escape(message, quote=False), - 'explain': html.escape(explain, quote=False) - }) - body = content.encode('UTF-8', 'replace') self.send_response(code, message) - self.send_header("Content-Type", self.error_content_type) self.send_header('Connection', 'close') - self.send_header('Content-Length', int(len(body))) + + body = None + if (code >= 200 and + code not in (HTTPStatus.NO_CONTENT, + HTTPStatus.RESET_CONTENT, + HTTPStatus.NOT_MODIFIED)): + # HTML encode to prevent Cross Site Scripting attacks + # (see bug #1100201) + content = (self.error_message_format % { + 'code': code, + 'message': html.escape(message, quote=False), + 'explain': html.escape(explain, quote=False) + }) + body = content.encode('UTF-8', 'replace') + self.send_header("Content-Type", self.error_content_type) + # Content-Length will not be sent for cases mentioned in below RFCs. + # - RFC7230: 3.3.2. 1xx, 204(No Content), 304(Not Modified) + # - RFC7231: 6.3.6. 205(Reset Content) + self.send_header('Content-Length', int(len(body))) self.end_headers() - if (self.command != 'HEAD' and - code >= 200 and - code not in ( - HTTPStatus.NO_CONTENT, HTTPStatus.NOT_MODIFIED)): + if self.command != 'HEAD' and body: self.wfile.write(body) def send_response(self, code, message=None): diff -r 0ddba0abab49 Lib/test/test_httpservers.py --- a/Lib/test/test_httpservers.py Thu Jun 02 10:13:47 2016 +0000 +++ b/Lib/test/test_httpservers.py Mon Jun 06 01:14:15 2016 -0400 @@ -116,6 +116,37 @@ body = self.headers['x-special-incoming'].encode('utf-8') self.wfile.write(body) + def do_NOCONTENT_VIA_SEND_ERROR(self): + self.send_error(HTTPStatus.NO_CONTENT) + + def do_RESETCONTENT_VIA_SEND_ERROR(self): + self.send_error(HTTPStatus.RESET_CONTENT) + + def do_NOTMODIFIED_VIA_SEND_ERROR(self): + self.send_error(HTTPStatus.NOT_MODIFIED) + + def do_SWITCHING_PROTOCOLS_VIA_SEND_ERROR(self): + self.send_error(HTTPStatus.SWITCHING_PROTOCOLS) + + def do_PROCESSING_VIA_SEND_ERROR(self): + self.send_error(HTTPStatus.PROCESSING) + + def do_HEAD(self): + if 'NO_CONTENT' in self.path: + self.send_error(HTTPStatus.NO_CONTENT) + return + elif 'RESET_CONTENT' in self.path: + self.send_error(HTTPStatus.RESET_CONTENT) + return + elif 'NOT_MODIFIED' in self.path: + self.send_error(HTTPStatus.NOT_MODIFIED) + return + elif 'SWITCHING_PROTOCOLS' in self.path: + # One example for 1xx code + self.send_error(HTTPStatus.SWITCHING_PROTOCOLS) + return + self.send_error(HTTPStatus.OK) + def setUp(self): BaseTestCase.setUp(self) self.con = http.client.HTTPConnection(self.HOST, self.PORT) @@ -237,6 +268,115 @@ data = res.read() self.assertEqual(int(res.getheader('Content-Length')), len(data)) + def test_no_content_via_send_error(self): + self.con.request('NOCONTENT_VIA_SEND_ERROR', '/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.NO_CONTENT, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + + data = res.read() + self.assertEqual(b'', data) + + def test_reset_content_via_send_error(self): + self.con.request('RESETCONTENT_VIA_SEND_ERROR', '/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.RESET_CONTENT, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + + data = res.read() + self.assertEqual(b'', data) + + def test_not_modified_via_send_error(self): + self.con.request('NOTMODIFIED_VIA_SEND_ERROR', '/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.NOT_MODIFIED, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + self.assertEqual(None, res.getheader('Transfer-Encoding')) + + data = res.read() + self.assertEqual(b'', data) + + def test_switching_protocols_via_send_error(self): + self.con.request('SWITCHING_PROTOCOLS_VIA_SEND_ERROR', '/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.SWITCHING_PROTOCOLS, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + self.assertEqual(None, res.getheader('Transfer-Encoding')) + + data = res.read() + self.assertEqual(b'', data) + + def test_processing_via_send_error(self): + self.con.request('PROCESSING_VIA_SEND_ERROR', '/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.PROCESSING, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + self.assertEqual(None, res.getheader('Transfer-Encoding')) + + data = res.read() + self.assertEqual(b'', data) + + def test_head_via_send_error(self): + self.con.request('HEAD', '/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.OK, res.status) + self.assertTrue(int(res.getheader('Content-Length')) > 0) + self.assertTrue('text/html' in res.getheader('Content-Type')) + + data = res.read() + self.assertEqual(b'', data) + + def test_no_content_head_via_send_error(self): + # Using URL to switch the code. + self.con.request('HEAD', 'NO_CONTENT/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.NO_CONTENT, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + + data = res.read() + self.assertEqual(b'', data) + + def test_reset_content_head_via_send_error(self): + # Using URL to switch the code. + self.con.request('HEAD', 'RESET_CONTENT/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.RESET_CONTENT, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + + data = res.read() + self.assertEqual(b'', data) + + def test_not_modified_head_via_send_error(self): + # Using URL to switch the code. + self.con.request('HEAD', 'NOT_MODIFIED/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.NOT_MODIFIED, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + self.assertEqual(None, res.getheader('Transfer-Encoding')) + + data = res.read() + self.assertEqual(b'', data) + + def test_switcing_protocols_head_via_send_error(self): + # Using URL to switch the code. + self.con.request('HEAD', 'SWITCHING_PROTOCOLS/') + res = self.con.getresponse() + self.assertEqual(HTTPStatus.SWITCHING_PROTOCOLS, res.status) + self.assertEqual(None, res.getheader('Content-Length')) + self.assertEqual(None, res.getheader('Content-Type')) + self.assertEqual(None, res.getheader('Transfer-Encoding')) + + data = res.read() + self.assertEqual(b'', data) + class RequestHandlerLoggingTestCase(BaseTestCase): class request_handler(BaseHTTPRequestHandler):