Ensure Content-Length in backend container/account HEAD response

A failing CORS test in the gate discovered that, when running with
eventlet==0.38.0, container and account HEAD requests returned
Content-Type of application/json to clients regardless of the requested
format. This was due to the backend HEAD response no longer having a
Content-Length header, causing the listing_formats middleware to not
modify the returned Content-Type (see Related-Change).

The Related-Change fixed the client facing issue by making
listing_formats middleware ensure the correct Content-Type is returned
to clients even when Content-Length is absent in the backend
response. The Related-Change also ensured that the 204 response to
clients always has a Content-Length header.

This patch directly fixes the problem of backend account and container
server HEADs no longer having 'Content-Length: 0' by adding it
explicitly. This violates the RFC prohibition of a 204 response having
a Content-Length header [1], but preserves Swift's historic behavior
and is consistent with the proxy-server's 204 response to clients.

[1] https://httpwg.org/specs/rfc7230.html#header.content-length
Related-Change: If724485e1425d1481d10b9255436301e346f07e8

Change-Id: Idacc59c5f43367926eff5221ee7fc417a9bc2d50
This commit is contained in:
Clay Gerrard 2024-11-20 10:59:43 -06:00 committed by Alistair Coles
parent fa889358ac
commit 4aadb54025
4 changed files with 61 additions and 1 deletions

View File

@ -230,6 +230,7 @@ class AccountController(BaseStorageServer):
return self._deleted_response(broker, req, HTTPNotFound)
headers = get_response_headers(broker)
headers['Content-Type'] = out_content_type
headers['Content-Length'] = 0
return HTTPNoContent(request=req, headers=headers, charset='utf-8')
@public

View File

@ -610,6 +610,7 @@ class ContainerController(BaseStorageServer):
if value != '' and (key.lower() in self.save_headers or
is_sys_or_user_meta('container', key)))
headers['Content-Type'] = out_content_type
headers['Content-Length'] = 0
resp = HTTPNoContent(request=req, headers=headers, charset='utf-8')
resp.last_modified = Timestamp(headers['X-PUT-Timestamp']).ceil()
return resp

View File

@ -324,6 +324,31 @@ class TestAccountController(unittest.TestCase):
self.assertEqual(resp.status_int, 404)
self.assertEqual(resp.headers['X-Account-Status'], 'Deleted')
def test_HEAD_has_content_length(self):
# create the account
put_timestamp = next(self.ts)
req = Request.blank('/sda1/p/a', method='PUT', headers={
'x-timestamp': put_timestamp.normal})
created_at_timestamp = next(self.ts)
with mock.patch('swift.account.backend.Timestamp.now',
return_value=created_at_timestamp):
resp = req.get_response(self.controller)
self.assertEqual(resp.status_int, 201)
# do a HEAD
req = Request.blank('/sda1/p/a', method='HEAD')
status, headers, body_iter = req.call_application(self.controller)
self.assertEqual('204 No Content', status)
self.assertEqual({
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '0',
'X-Account-Container-Count': '0',
'X-Account-Object-Count': '0',
'X-Account-Bytes-Used': '0',
'X-Timestamp': created_at_timestamp.normal,
'X-Put-Timestamp': put_timestamp.normal,
}, dict(headers))
self.assertEqual(b'', b''.join(body_iter))
def test_HEAD_empty_account(self):
req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT',
'HTTP_X_TIMESTAMP': '0'})

View File

@ -37,7 +37,7 @@ from six.moves.urllib.parse import quote
from swift import __version__ as swift_version
from swift.common.header_key_dict import HeaderKeyDict
from swift.common.swob import (Request, WsgiBytesIO, HTTPNoContent,
bytes_to_wsgi)
bytes_to_wsgi, Response)
import swift.container
from swift.container import server as container_server
from swift.common import constraints
@ -203,6 +203,39 @@ class TestContainerController(unittest.TestCase):
self.assertEqual(response.headers.get('x-container-write'),
'account:user')
def test_HEAD_has_content_length(self):
# create a container
put_timestamp = next(self.ts)
expected_last_modified = Response(
last_modified=put_timestamp.ceil()).headers['Last-Modified']
created_at_timestamp = next(self.ts)
req = Request.blank('/sda1/p/a/c', method='PUT', headers={
'x-timestamp': put_timestamp.normal})
with mock.patch('swift.container.backend.Timestamp.now',
return_value=created_at_timestamp):
resp = req.get_response(self.controller)
self.assertEqual(resp.status_int, 201)
# do a HEAD
req = Request.blank('/sda1/p/a/c', method='HEAD')
status, headers, body_iter = req.call_application(self.controller)
self.assertEqual('204 No Content', status)
self.assertEqual({
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '0',
'Last-Modified': expected_last_modified,
'X-Backend-Delete-Timestamp': '0000000000.00000',
'X-Backend-Put-Timestamp': put_timestamp.normal,
'X-Backend-Sharding-State': 'unsharded',
'X-Backend-Status-Changed-At': put_timestamp.normal,
'X-Backend-Storage-Policy-Index': str(int(POLICIES.default)),
'X-Backend-Timestamp': created_at_timestamp.normal,
'X-Container-Bytes-Used': '0',
'X-Container-Object-Count': '0',
'X-Put-Timestamp': put_timestamp.normal,
'X-Timestamp': created_at_timestamp.normal,
}, dict(headers))
self.assertEqual(b'', b''.join(body_iter))
def _test_head(self, start, ts):
req = Request.blank('/sda1/p/a/c', method='HEAD')
response = req.get_response(self.controller)