diff --git a/swift/common/middleware/slo.py b/swift/common/middleware/slo.py index 8b7fd911d9..25e5c643c8 100644 --- a/swift/common/middleware/slo.py +++ b/swift/common/middleware/slo.py @@ -595,7 +595,7 @@ class RespAttrs(object): self.is_legacy = not self._has_size_and_etag() def _has_size_and_etag(self): - return self.slo_size > 0 and self.slo_etag + return self.slo_size >= 0 and self.slo_etag @classmethod def from_headers(cls, response_headers): diff --git a/test/unit/common/middleware/test_slo.py b/test/unit/common/middleware/test_slo.py index 21f170fb1a..acc48d6a61 100644 --- a/test/unit/common/middleware/test_slo.py +++ b/test/unit/common/middleware/test_slo.py @@ -2238,6 +2238,30 @@ class TestSloHeadOldManifest(SloGETorHEADTestCase): # *add* it on SLO requests, not a multipart-manifest=get request self.assertNotIn('X-Backend-Etag-Is-At', self.app.headers[0]) + def test_zero_byte_manifest(self): + _single_segment_manifest = [ + {'name': '/c/zero', 'hash': md5hex(''), 'bytes': '0', + 'content_type': 'text/plain'}, + ] + self._setup_manifest('zero-byte', _single_segment_manifest) + req = Request.blank('/v1/AUTH_test/c/manifest-zero-byte', + method='HEAD') + status, headers, body = self.call_slo(req) + self.assertEqual(status, '200 OK') + self.assertEqual(headers['Etag'], + '"%s"' % self.manifest_zero_byte_slo_etag) + self.assertEqual(headers['Content-Length'], '0') + self.assertEqual(headers['X-Static-Large-Object'], 'true') + self.assertEqual(headers['X-Manifest-Etag'], + self.manifest_zero_byte_json_md5) + self.assertEqual(body, b'') # it's a HEAD request, after all + + expected_app_calls = [('HEAD', '/v1/AUTH_test/c/manifest-zero-byte')] + if not self.modern_manifest_headers: + expected_app_calls.append(( + 'GET', '/v1/AUTH_test/c/manifest-zero-byte')) + self.assertEqual(self.app.calls, expected_app_calls) + def test_if_none_match_etag_matching(self): req = Request.blank( '/v1/AUTH_test/headtest/man', @@ -6105,6 +6129,29 @@ class TestRespAttrs(unittest.TestCase): # see lp bug #2035158 self.assertFalse(attrs.is_legacy) + def test_from_zero_byte_sysmeta(self): + attrs = slo.RespAttrs.from_headers([ + ('X-Backend-Timestamp', '1709069771.34178'), + ('X-Object-Sysmeta-Container-Update-Override-Etag', + 'a1eadf0ca181e87fcbdba2074ce0fd90; ' + 's3_etag=59adb24ef3cdbe0297f05b395827453f-1; ' + 'slo_etag=74be16979710d4c4e7c6647856088456'), + ('X-Object-Sysmeta-S3Api-Etag', + '59adb24ef3cdbe0297f05b395827453f-1'), + ('X-Object-Sysmeta-S3Api-Upload-Id', + 'NDZlMDBhN2MtNzVmZS00ZTljLTkzN2EtODcwNGQ5OTg4NmQ2'), + ('X-Object-Sysmeta-Slo-Etag', '74be16979710d4c4e7c6647856088456'), + ('X-Object-Sysmeta-Slo-Size', '0'), + ('ETag', 'a1eadf0ca181e87fcbdba2074ce0fd90'), + ('X-Static-Large-Object', 'True'), + ]) + self.assertTrue(attrs.is_slo) + self.assertEqual(1709069771.34178, attrs.timestamp) + self.assertEqual('a1eadf0ca181e87fcbdba2074ce0fd90', attrs.json_md5) + self.assertEqual('74be16979710d4c4e7c6647856088456', attrs.slo_etag) + self.assertEqual(0, attrs.slo_size) + self.assertFalse(attrs.is_legacy) + def _legacy_from_headers(self): attrs = slo.RespAttrs.from_headers( [('X-Backend-Timestamp', '123456789.12345'),