bufferedhttp: Tolerate socket being None

Seen in prod:

Traceback (most recent call last):
  File ".../swift/proxy/controllers/obj.py", line 416, in _get_conn_response
    self.app.node_timeout, not final_phase)
  File ".../swift/proxy/controllers/obj.py", line 1645, in await_response
    self.resp = self.conn.getexpect()
  File ".../swift/common/bufferedhttp.py", line 229, in getexpect
    response = BufferedHTTPResponse(self.sock, **kwargs)
  File ".../swift/common/bufferedhttp.py", line 64, in __init__
    self._real_socket = sock.fd._sock
AttributeError: 'NoneType' object has no attribute 'fd'

Change-Id: I35b3660f954bcf91aee2698b2c9cc10d5447abc1
Co-Authored-By: Clay Gerrard <clay.gerrard@gmail.com>
This commit is contained in:
Tim Burke 2021-06-15 14:41:01 -07:00
parent 977f1bcc61
commit 50ba8e6975
2 changed files with 58 additions and 4 deletions

View File

@ -33,7 +33,7 @@ import socket
import eventlet
from eventlet.green.httplib import CONTINUE, HTTPConnection, HTTPMessage, \
HTTPResponse, HTTPSConnection, _UNKNOWN
HTTPResponse, HTTPSConnection, _UNKNOWN, ImproperConnectionState
from six.moves.urllib.parse import quote, parse_qsl, urlencode
import six
@ -56,16 +56,24 @@ class BufferedHTTPResponse(HTTPResponse):
def __init__(self, sock, debuglevel=0, strict=0,
method=None): # pragma: no cover
# sock should be an eventlet.greenio.GreenSocket
self.sock = sock
# sock is an eventlet.greenio.GreenSocket
if six.PY2:
if sock is None:
# ...but it could be None if we close the connection as we're
# getting it wrapped up in a Response
self._real_socket = None
# No socket means no file-like -- set it to None like in
# HTTPResponse.close()
self.fp = None
elif six.PY2:
# sock.fd is a socket._socketobject
# sock.fd._sock is a _socket.socket object, which is what we want.
self._real_socket = sock.fd._sock
self.fp = sock.makefile('rb')
else:
# sock.fd is a socket.socket, which should have a _real_close
self._real_socket = sock.fd
self.fp = sock.makefile('rb')
self.fp = sock.makefile('rb')
self.debuglevel = debuglevel
self.strict = strict
self._method = method
@ -106,6 +114,8 @@ class BufferedHTTPResponse(HTTPResponse):
if self.fp:
self.fp.close()
self.fp = None
if not self.sock:
raise ImproperConnectionState('Socket already closed')
self.fp = self.sock.makefile('rb', 0)
version, status, reason = self._read_status()
if status != CONTINUE:

View File

@ -99,6 +99,50 @@ class TestBufferedHTTP(unittest.TestCase):
if err:
raise Exception(err)
def test_get_expect(self):
bindsock = listen_zero()
request = []
def accept():
with Timeout(3):
sock, addr = bindsock.accept()
fp = sock.makefile('rwb')
request.append(fp.readline())
fp.write(b'HTTP/1.1 100 Continue\r\n\r\n')
fp.flush()
fp.write(b'HTTP/1.1 200 OK\r\nContent-Length: 8\r\n\r\n'
b'RESPONSE')
fp.flush()
server = spawn(accept)
try:
address = '%s:%s' % ('127.0.0.1', bindsock.getsockname()[1])
conn = bufferedhttp.BufferedHTTPConnection(address)
conn.putrequest('GET', '/path')
conn.endheaders()
resp = conn.getexpect()
self.assertIsInstance(resp, bufferedhttp.BufferedHTTPResponse)
self.assertEqual(resp.status, 100)
self.assertEqual(resp.version, 11)
self.assertEqual(resp.reason, 'Continue')
# I don't think you're supposed to "read" a continue response
self.assertRaises(AssertionError, resp.read)
resp = conn.getresponse()
self.assertIsInstance(resp, bufferedhttp.BufferedHTTPResponse)
self.assertEqual(resp.read(), b'RESPONSE')
finally:
server.wait()
self.assertEqual(request[0], b'GET /path HTTP/1.1\r\n')
def test_closed_response(self):
resp = bufferedhttp.BufferedHTTPResponse(None)
self.assertEqual(resp.status, 'UNKNOWN')
self.assertEqual(resp.version, 'UNKNOWN')
self.assertEqual(resp.reason, 'UNKNOWN')
self.assertEqual(resp.read(), b'')
def test_nonstr_header_values(self):
with mock.patch('swift.common.bufferedhttp.HTTPSConnection',
MockHTTPSConnection):