# HG changeset patch # Parent 0ca0287654880cdc18ce6acc6992dfb00d1e2400 diff -r 0ca028765488 Doc/library/http.client.rst --- a/Doc/library/http.client.rst Wed Apr 08 16:53:21 2015 +0100 +++ b/Doc/library/http.client.rst Thu Apr 09 08:15:58 2015 +0000 @@ -93,7 +93,7 @@ parameter. -.. class:: HTTPResponse(sock, debuglevel=0, method=None, url=None) +.. class:: HTTPResponse(fp, debuglevel=0, method=None, url=None) Class whose instances are returned upon successful connection. Not instantiated directly by user. diff -r 0ca028765488 Lib/http/client.py --- a/Lib/http/client.py Wed Apr 08 16:53:21 2015 +0100 +++ b/Lib/http/client.py Thu Apr 09 08:15:58 2015 +0000 @@ -209,7 +209,7 @@ # text following RFC 2047. The basic status line parsing only # accepts iso-8859-1. - def __init__(self, sock, debuglevel=0, method=None, url=None): + def __init__(self, fp, debuglevel=0, method=None, url=None): # If the response includes a content-length header, we need to # make sure that the client doesn't read more than the # specified number of bytes. If it does, it will block until @@ -217,7 +217,7 @@ # happen if a self.fp.read() is done (without a size) whether # self.fp is buffered or not. So, no self.fp.read() by # clients unless they know what they are doing. - self.fp = sock.makefile("rb") + self.fp = fp self.debuglevel = debuglevel self._method = method @@ -237,7 +237,7 @@ self.chunked = _UNKNOWN # is "chunked" being used? self.chunk_left = _UNKNOWN # bytes left to read in current chunk self.length = _UNKNOWN # number of bytes left in response - self.will_close = _UNKNOWN # conn will close at end of response + self.will_close = _UNKNOWN # fp not shared with HTTPConnection def _read_status(self): line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") @@ -385,7 +385,8 @@ def _close_conn(self): fp = self.fp self.fp = None - fp.close() + if self.will_close: + fp.close() def close(self): super().close() # set "closed" flag @@ -397,11 +398,6 @@ # XXX This class should probably be revised to act more like # the "raw stream" that BufferedReader expects. - def flush(self): - super().flush() - if self.fp: - self.fp.flush() - def readable(self): return True @@ -727,7 +723,8 @@ self.source_address = source_address self.sock = None self._buffer = [] - self.__response = None + self._reader = None + self.__response = None # Response object when connection is shared self.__state = _CS_IDLE self._method = None self._tunnel_host = None @@ -798,7 +795,8 @@ self.send(header_bytes) self.send(b'\r\n') - response = self.response_class(self.sock, method=self._method) + self._reader = self.sock.makefile("rb") + response = self.response_class(self._reader, method=self._method) (version, code, message) = response._read_status() if code != http.HTTPStatus.OK: @@ -829,14 +827,25 @@ def close(self): """Close the connection to the HTTP server.""" + if self._reader: + self._reader.close() + self._reader = None if self.sock: - self.sock.close() # close it manually... there may be other refs + self.sock.close() self.sock = None if self.__response: self.__response.close() self.__response = None self.__state = _CS_IDLE + def _disown(self): + """Pass ownership of the connection to the response object.""" + if self.__response: + self.__response.will_close = True + self.__response = None + self._reader = None + self.close() + def send(self, data): """Send `data' to the server. ``data`` can be a string object, a bytes object, an array object, a @@ -921,8 +930,8 @@ self.__response = None - # in certain cases, we cannot issue another request on this connection. - # this occurs when: + # In certain cases, we cannot issue another request on a previously- + # used connection. This occurs when: # 1) we are in the process of sending a request. (_CS_REQ_STARTED) # 2) a response to a previous request has signalled that it is going # to close the connection upon completion. @@ -1131,8 +1140,8 @@ If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then - it will be closed before the response is returned. When the - connection is closed, the underlying socket is closed. + the HTTPConnection object will detach itself from this connection, + and a subsequent request will trigger a new connection. """ # if a prior response has been completed, then forget about it. @@ -1157,11 +1166,13 @@ if self.__state != _CS_REQ_SENT or self.__response: raise ResponseNotReady(self.__state) + if self._reader is None: + self._reader = self.sock.makefile("rb") if self.debuglevel > 0: - response = self.response_class(self.sock, self.debuglevel, + response = self.response_class(self._reader, self.debuglevel, method=self._method) else: - response = self.response_class(self.sock, method=self._method) + response = self.response_class(self._reader, method=self._method) try: try: @@ -1173,8 +1184,7 @@ self.__state = _CS_IDLE if response.will_close: - # this effectively passes the connection to the response - self.close() + self._disown() else: # remember this, so we can tell when it is complete self.__response = response diff -r 0ca028765488 Lib/test/test_httplib.py --- a/Lib/test/test_httplib.py Wed Apr 08 16:53:21 2015 +0100 +++ b/Lib/test/test_httplib.py Thu Apr 09 08:15:58 2015 +0000 @@ -5,6 +5,7 @@ import os import array import socket +from threading import Thread import unittest TestCase = unittest.TestCase @@ -124,6 +125,12 @@ def create_connection(self, *pos, **kw): return FakeSocket(*self.fake_socket_args) +class StringHTTPResponse(client.HTTPResponse): + """HTTPResponse reading from a byte string rather than a socket""" + def __init__(self, string, *pos, **kw): + reader = io.BytesIO(string.encode('ascii')) + super().__init__(reader, *pos, **kw) + class HeaderTests(TestCase): def test_auto_headers(self): # Some headers are added automatically, but should not be added by @@ -276,8 +283,7 @@ def test_malformed_headers_coped_with(self): # Issue 19996 body = "HTTP/1.1 200 OK\r\nFirst: val\r\n: nval\r\nSecond: val\r\n\r\n" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() self.assertEqual(resp.getheader('First'), 'val') @@ -319,8 +325,7 @@ # Test HTTP status lines body = "HTTP/1.1 200 Ok\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() self.assertEqual(resp.read(0), b'') # Issue #20007 self.assertFalse(resp.isclosed()) @@ -332,8 +337,7 @@ self.assertTrue(resp.closed) body = "HTTP/1.1 400.100 Not Ok\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) self.assertRaises(client.BadStatusLine, resp.begin) def test_bad_status_repr(self): @@ -344,8 +348,7 @@ # if we have a length, the system knows when to close itself # same behaviour than when we read the whole thing with read() body = "HTTP/1.1 200 Ok\r\nContent-Length: 4\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() self.assertEqual(resp.read(2), b'Te') self.assertFalse(resp.isclosed()) @@ -359,8 +362,7 @@ # if we have a length, the system knows when to close itself # same behaviour than when we read the whole thing with read() body = "HTTP/1.1 200 Ok\r\nContent-Length: 4\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() b = bytearray(2) n = resp.readinto(b) @@ -379,8 +381,7 @@ # when no length is present, the socket should be gracefully closed when # all data was read body = "HTTP/1.1 200 Ok\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() self.assertEqual(resp.read(2), b'Te') self.assertFalse(resp.isclosed()) @@ -395,8 +396,7 @@ # when no length is present, the socket should be gracefully closed when # all data was read body = "HTTP/1.1 200 Ok\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() b = bytearray(2) n = resp.readinto(b) @@ -414,8 +414,7 @@ # if the server shuts down the connection before the whole # content-length is delivered, the socket is gracefully closed body = "HTTP/1.1 200 Ok\r\nContent-Length: 10\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() self.assertEqual(resp.read(2), b'Te') self.assertFalse(resp.isclosed()) @@ -427,8 +426,7 @@ # if the server shuts down the connection before the whole # content-length is delivered, the socket is gracefully closed body = "HTTP/1.1 200 Ok\r\nContent-Length: 10\r\n\r\nText" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() b = bytearray(2) n = resp.readinto(b) @@ -474,8 +472,7 @@ hdr = ('Customer="WILE_E_COYOTE"; Version="1"; Path="/acme"' ', ' 'Part_Number="Rocket_Launcher_0001"; Version="1"; Path="/acme"') - s = FakeSocket(text) - r = client.HTTPResponse(s) + r = StringHTTPResponse(text) r.begin() cookies = r.getheader("Set-Cookie") self.assertEqual(cookies, hdr) @@ -483,12 +480,11 @@ def test_read_head(self): # Test that the library doesn't attempt to read any data # from a HEAD request. (Tickles SF bug #622042.) - sock = FakeSocket( - 'HTTP/1.1 200 OK\r\n' - 'Content-Length: 14432\r\n' - '\r\n', - NoEOFBytesIO) - resp = client.HTTPResponse(sock, method="HEAD") + reader = NoEOFBytesIO( + b'HTTP/1.1 200 OK\r\n' + b'Content-Length: 14432\r\n' + b'\r\n') + resp = client.HTTPResponse(reader, method="HEAD") resp.begin() if resp.read(): self.fail("Did not expect response from HEAD request") @@ -496,12 +492,11 @@ def test_readinto_head(self): # Test that the library doesn't attempt to read any data # from a HEAD request. (Tickles SF bug #622042.) - sock = FakeSocket( - 'HTTP/1.1 200 OK\r\n' - 'Content-Length: 14432\r\n' - '\r\n', - NoEOFBytesIO) - resp = client.HTTPResponse(sock, method="HEAD") + reader = NoEOFBytesIO( + b'HTTP/1.1 200 OK\r\n' + b'Content-Length: 14432\r\n' + b'\r\n') + resp = client.HTTPResponse(reader, method="HEAD") resp.begin() b = bytearray(5) if resp.readinto(b) != 0: @@ -512,8 +507,7 @@ headers = '\r\n'.join('Header%d: foo' % i for i in range(client._MAXHEADERS + 1)) + '\r\n' text = ('HTTP/1.1 200 OK\r\n' + headers) - s = FakeSocket(text) - r = client.HTTPResponse(s) + r = StringHTTPResponse(text) self.assertRaisesRegex(client.HTTPException, r"got more than \d+ headers", r.begin) @@ -589,23 +583,23 @@ def test_chunked(self): expected = chunked_expected - sock = FakeSocket(chunked_start + last_chunk + chunked_end) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + last_chunk + chunked_end + resp = StringHTTPResponse(input, method="GET") resp.begin() self.assertEqual(resp.read(), expected) resp.close() # Various read sizes for n in range(1, 12): - sock = FakeSocket(chunked_start + last_chunk + chunked_end) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + last_chunk + chunked_end + resp = StringHTTPResponse(input, method="GET") resp.begin() self.assertEqual(resp.read(n) + resp.read(n) + resp.read(), expected) resp.close() for x in ('', 'foo\r\n'): - sock = FakeSocket(chunked_start + x) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + x + resp = StringHTTPResponse(input, method="GET") resp.begin() try: resp.read() @@ -625,8 +619,8 @@ nexpected = len(expected) b = bytearray(128) - sock = FakeSocket(chunked_start + last_chunk + chunked_end) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + last_chunk + chunked_end + resp = StringHTTPResponse(input, method="GET") resp.begin() n = resp.readinto(b) self.assertEqual(b[:nexpected], expected) @@ -635,8 +629,8 @@ # Various read sizes for n in range(1, 12): - sock = FakeSocket(chunked_start + last_chunk + chunked_end) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + last_chunk + chunked_end + resp = StringHTTPResponse(input, method="GET") resp.begin() m = memoryview(b) i = resp.readinto(m[0:n]) @@ -647,8 +641,8 @@ resp.close() for x in ('', 'foo\r\n'): - sock = FakeSocket(chunked_start + x) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + x + resp = StringHTTPResponse(input, method="GET") resp.begin() try: n = resp.readinto(b) @@ -671,8 +665,8 @@ '1\r\n' 'd\r\n' ) - sock = FakeSocket(chunked_start + last_chunk + chunked_end) - resp = client.HTTPResponse(sock, method="HEAD") + input = chunked_start + last_chunk + chunked_end + resp = StringHTTPResponse(input, method="HEAD") resp.begin() self.assertEqual(resp.read(), b'') self.assertEqual(resp.status, 200) @@ -691,8 +685,8 @@ '1\r\n' 'd\r\n' ) - sock = FakeSocket(chunked_start + last_chunk + chunked_end) - resp = client.HTTPResponse(sock, method="HEAD") + input = chunked_start + last_chunk + chunked_end + resp = StringHTTPResponse(input, method="HEAD") resp.begin() b = bytearray(5) n = resp.readinto(b) @@ -706,16 +700,15 @@ self.assertTrue(resp.closed) def test_negative_content_length(self): - sock = FakeSocket( - 'HTTP/1.1 200 OK\r\nContent-Length: -1\r\n\r\nHello\r\n') - resp = client.HTTPResponse(sock, method="GET") + input = 'HTTP/1.1 200 OK\r\nContent-Length: -1\r\n\r\nHello\r\n' + resp = StringHTTPResponse(input, method="GET") resp.begin() self.assertEqual(resp.read(), b'Hello\r\n') self.assertTrue(resp.isclosed()) def test_incomplete_read(self): - sock = FakeSocket('HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nHello\r\n') - resp = client.HTTPResponse(sock, method="GET") + input = 'HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nHello\r\n' + resp = StringHTTPResponse(input, method="GET") resp.begin() try: resp.read() @@ -748,7 +741,7 @@ def test_overflowing_status_line(self): body = "HTTP/1.1 200 Ok" + "k" * 65536 + "\r\n" - resp = client.HTTPResponse(FakeSocket(body)) + resp = StringHTTPResponse(body) self.assertRaises((client.LineTooLong, client.BadStatusLine), resp.begin) def test_overflowing_header_line(self): @@ -756,7 +749,7 @@ 'HTTP/1.1 200 OK\r\n' 'X-Foo: bar' + 'r' * 65536 + '\r\n\r\n' ) - resp = client.HTTPResponse(FakeSocket(body)) + resp = StringHTTPResponse(body) self.assertRaises(client.LineTooLong, resp.begin) def test_overflowing_chunked_line(self): @@ -768,15 +761,14 @@ '0\r\n' '\r\n' ) - resp = client.HTTPResponse(FakeSocket(body)) + resp = StringHTTPResponse(body) resp.begin() self.assertRaises(client.LineTooLong, resp.read) def test_early_eof(self): # Test httpresponse with no \r\n termination, body = "HTTP/1.1 200 Ok" - sock = FakeSocket(body) - resp = client.HTTPResponse(sock) + resp = StringHTTPResponse(body) resp.begin() self.assertEqual(resp.read(), b'') self.assertTrue(resp.isclosed()) @@ -804,8 +796,8 @@ extra = '3;foo=bar\r\n' + 'abc\r\n' expected = chunked_expected + b'abc' - sock = FakeSocket(chunked_start + extra + last_chunk_extended + chunked_end) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + extra + last_chunk_extended + chunked_end + resp = StringHTTPResponse(input, method="GET") resp.begin() self.assertEqual(resp.read(), expected) resp.close() @@ -813,8 +805,8 @@ def test_chunked_missing_end(self): """some servers may serve up a short chunked encoding stream""" expected = chunked_expected - sock = FakeSocket(chunked_start + last_chunk) #no terminating crlf - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + last_chunk #no terminating crlf + resp = StringHTTPResponse(input, method="GET") resp.begin() self.assertEqual(resp.read(), expected) resp.close() @@ -822,36 +814,38 @@ def test_chunked_trailers(self): """See that trailers are read and ignored""" expected = chunked_expected - sock = FakeSocket(chunked_start + last_chunk + trailers + chunked_end) - resp = client.HTTPResponse(sock, method="GET") + input = chunked_start + last_chunk + trailers + chunked_end + reader = io.BytesIO(input.encode("ascii")) + resp = client.HTTPResponse(reader, method="GET") resp.begin() self.assertEqual(resp.read(), expected) # we should have reached the end of the file - self.assertEqual(sock.file.read(100), b"") #we read to the end + self.assertEqual(reader.read(100), b"") #we read to the end resp.close() def test_chunked_sync(self): """Check that we don't read past the end of the chunked-encoding stream""" expected = chunked_expected - extradata = "extradata" - sock = FakeSocket(chunked_start + last_chunk + trailers + chunked_end + extradata) - resp = client.HTTPResponse(sock, method="GET") + extradata = b"extradata" + chunked = chunked_start + last_chunk + trailers + chunked_end + reader = io.BytesIO(chunked.encode("ascii") + extradata) + resp = client.HTTPResponse(reader, method="GET") resp.begin() self.assertEqual(resp.read(), expected) # the file should now have our extradata ready to be read - self.assertEqual(sock.file.read(100), extradata.encode("ascii")) #we read to the end + self.assertEqual(reader.read(100), extradata) #we read to the end resp.close() def test_content_length_sync(self): """Check that we don't read past the end of the Content-Length stream""" - extradata = "extradata" + extradata = b"extradata" expected = b"Hello123\r\n" - sock = FakeSocket('HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nHello123\r\n' + extradata) - resp = client.HTTPResponse(sock, method="GET") + reader = io.BytesIO(b'HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nHello123\r\n' + extradata) + resp = client.HTTPResponse(reader, method="GET") resp.begin() self.assertEqual(resp.read(), expected) # the file should now have our extradata ready to be read - self.assertEqual(sock.file.read(100), extradata.encode("ascii")) #we read to the end + self.assertEqual(reader.read(100), extradata) #we read to the end resp.close() class ExtendedReadTest(TestCase): @@ -885,8 +879,7 @@ ) def setUp(self): - sock = FakeSocket(self.lines) - resp = client.HTTPResponse(sock, method="GET") + resp = StringHTTPResponse(self.lines, method="GET") resp.begin() resp.fp = io.BufferedReader(resp.fp) self.resp = resp @@ -1145,12 +1138,12 @@ # for an ssl_wrapped connect() to actually return from. -class TimeoutTest(TestCase): - PORT = None +class ServerTest(TestCase): + """Tests that use a real HTTP server socket""" def setUp(self): self.serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - TimeoutTest.PORT = support.bind_port(self.serv) + self.PORT = support.bind_port(self.serv) self.serv.listen() def tearDown(self): @@ -1165,7 +1158,7 @@ self.assertIsNone(socket.getdefaulttimeout()) socket.setdefaulttimeout(30) try: - httpConn = client.HTTPConnection(HOST, TimeoutTest.PORT) + httpConn = client.HTTPConnection(HOST, self.PORT) httpConn.connect() finally: socket.setdefaulttimeout(None) @@ -1176,7 +1169,7 @@ self.assertIsNone(socket.getdefaulttimeout()) socket.setdefaulttimeout(30) try: - httpConn = client.HTTPConnection(HOST, TimeoutTest.PORT, + httpConn = client.HTTPConnection(HOST, self.PORT, timeout=None) httpConn.connect() finally: @@ -1185,11 +1178,64 @@ httpConn.close() # a value - httpConn = client.HTTPConnection(HOST, TimeoutTest.PORT, timeout=30) + httpConn = client.HTTPConnection(HOST, self.PORT, timeout=30) httpConn.connect() self.assertEqual(httpConn.sock.gettimeout(), 30) httpConn.close() + def testDoubleResponse(self): + # Test when a server's response is followed by another in the same + # send() call + httpConn = client.HTTPConnection(HOST, self.PORT) + background = None + try: + serverSocket = None + serverReader = None + def startFirstResponse(): + nonlocal serverSocket, serverReader + [serverSocket, _] = self.serv.accept() + serverReader = serverSocket.makefile('rb') + while serverReader.readline().rstrip(b'\r\n'): + pass + serverSocket.sendall( + b'HTTP/1.1 200 First response\r\n' + b'Content-Length: 5\r\n' + b'\r\n' + ) + background = Thread(target=startFirstResponse) + background.start() + httpConn.request('GET', '/first') + with httpConn.getresponse() as response: + background.join() + self.assertEqual(response.reason, 'First response') + + def finishBothResponses(): + with serverSocket: + with serverReader: + while serverReader.readline().rstrip(b'\r\n'): + pass + serverSocket.sendall( + b'ABC\r\n' # First response's body + + b'HTTP/1.1 200 Second response\r\n' + b'Content-Length: 6\r\n' + b'\r\n' + b'Done\r\n' + ) + background = Thread(target=finishBothResponses) + background.start() + # Pipeline second request before reading first body, so that + # server can immediately follow it with the second response + httpConn.request('GET', '/second') + self.assertEqual(response.read(), b'ABC\r\n') + with httpConn.getresponse() as response: + self.assertEqual(response.reason, 'Second response') + self.assertEqual(response.read(), b'Done\r\n') + finally: + httpConn.close() + if background: + background.join() + class PersistenceTest(TestCase): @@ -1275,7 +1321,7 @@ def test_attributes(self): # simple test to check it's storing the timeout - h = client.HTTPSConnection(HOST, TimeoutTest.PORT, timeout=30) + h = client.HTTPSConnection(HOST, 80, timeout=30) self.assertEqual(h.timeout, 30) def test_networked(self): @@ -1496,8 +1542,7 @@ def setUp(self): body = "HTTP/1.1 200 Ok\r\nMy-Header: first-value\r\nMy-Header: \ second-value\r\n\r\nText" - sock = FakeSocket(body) - self.resp = client.HTTPResponse(sock) + self.resp = StringHTTPResponse(body) self.resp.begin() def test_getting_header(self): @@ -1601,7 +1646,7 @@ @support.reap_threads def test_main(verbose=None): - support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest, + support.run_unittest(HeaderTests, OfflineTest, BasicTest, ServerTest, PersistenceTest, HTTPSTest, RequestBodyTest, SourceAddressTest, HTTPResponseTest, ExtendedReadTest, diff -r 0ca028765488 Lib/test/test_urllib2.py --- a/Lib/test/test_urllib2.py Wed Apr 08 16:53:21 2015 +0100 +++ b/Lib/test/test_urllib2.py Thu Apr 09 08:15:58 2015 +0000 @@ -329,6 +329,8 @@ def close(self): pass + def _disown(self): + pass class MockHandler: # useful for testing handler machinery diff -r 0ca028765488 Lib/urllib/request.py --- a/Lib/urllib/request.py Wed Apr 08 16:53:21 2015 +0100 +++ b/Lib/urllib/request.py Thu Apr 09 08:15:58 2015 +0000 @@ -1205,9 +1205,7 @@ # If the server does not send us a 'Connection: close' header, # HTTPConnection assumes the socket should be left open. Manually # mark the socket to be closed when this response object goes away. - if h.sock: - h.sock.close() - h.sock = None + h._disown() r.url = req.get_full_url() # This line replaces the .msg attribute of the HTTPResponse