Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-HTTP-97508.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "bugfix",
"category": "HTTP",
"description": "Fixed an issue where reused connections could return a cached response status, dropping response headers."
}
2 changes: 1 addition & 1 deletion awscli/botocore/awsrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ def request(self, method, url, body=None, headers=None, *args, **kwargs):
if headers is None:
headers = {}
self._response_received = False
self.response_class = self._original_response_cls
if headers.get('Expect', b'') == b'100-continue':
self._expect_header_set = True
else:
self._expect_header_set = False
self.response_class = self._original_response_cls
rval = super().request(method, url, body, headers, *args, **kwargs)
self._expect_header_set = False
return rval
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/botocore/test_awsrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,52 @@ def test_state_reset_on_connection_close(self):
# current response.,
self.assertEqual(response.status, 200)

def test_early_final_response_does_not_corrupt_next_response(self):
# Request A is a 0-byte upload with Expect: 100-continue. Because the
# body is empty, the server skips "100 Continue" and returns the final
# 200 immediately. _handle_expect_response caches that status via
# response_class -> the connection is now poisoned.
#
# Request B reuses the SAME connection WITHOUT close().
# request() should reset response_class

with mock.patch('urllib3.util.wait_for_read') as wait_mock:
wait_mock.return_value = True
conn = AWSHTTPConnection('s3.amazonaws.com', 443)

# Request A: 0-byte upload -> early final 200 (no 100 Continue).
# Content-Length: 0 keeps the connection open (not auto-closed), so
# the poisoned response_class persists for the next request.
conn.sock = FakeSocket(
b'HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n'
)
conn.request(
'PUT', '/empty-object', b'', {'Expect': b'100-continue'}
)
response_a = conn.getresponse()
self.assertEqual(response_a.status, 200)
response_a.read() # drain so the connection can be reused

# Request B: a normal non-empty upload reusing the connection
# (no close()). The server does the normal 100 -> 200 with an ETag.
conn.sock = FakeSocket(
b'HTTP/1.1 100 (Continue)\r\n\r\n'
b'HTTP/1.1 200 OK\r\n'
b'ETag: "test-etag"\r\n'
b'Content-Length: 0\r\n'
b'\r\n'
)
conn.request(
'PUT', '/real-object', b'body', {'Expect': b'100-continue'}
)
response_b = conn.getresponse()

# Status is 200 either way (the leaked cached status is also 200),
# so this is only a sanity check. It should have a proper parsed
# Etag
self.assertEqual(response_b.status, 200)
self.assertEqual(response_b.headers.get('ETag'), '"test-etag"')


class TestAWSHTTPConnectionPool(unittest.TestCase):
def test_global_urllib3_pool_is_unchanged(self):
Expand Down
Loading