diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java index 518a9b772da..96229c002a4 100644 --- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java +++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java @@ -156,6 +156,20 @@ OzoneVolume getVolumeDetails(String volumeName) */ OzoneKey headS3Object(String bucketName, String keyName) throws IOException; + /** + * Look up metadata for a single part of a multipart object in S3 context. + * Uses HEAD semantics (no block tokens or pipeline refresh), while still + * validating the requested part number. + * + * @param bucketName Name of the Bucket + * @param keyName Key name + * @param partNumber Multipart-upload part number + * @return {@link OzoneKey} for the requested part + * @throws IOException + */ + OzoneKey headS3Object(String bucketName, String keyName, int partNumber) + throws IOException; + /** * Get OzoneKey in S3 context. * @param bucketName Name of the Bucket diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java index 57d358e393e..d56fa898361 100644 --- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java +++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java @@ -1864,11 +1864,24 @@ public OzoneKeyDetails getS3KeyDetails(String bucketName, String keyName) @Override public OzoneKeyDetails getS3KeyDetails(String bucketName, String keyName, int partNumber) throws IOException { + return getOzoneKeyDetails( + getS3PartOmKeyInfo(bucketName, keyName, partNumber, false)); + } + + @Override + public OzoneKey headS3Object(String bucketName, String keyName, + int partNumber) throws IOException { + return OzoneKey.fromKeyInfo( + getS3PartOmKeyInfo(bucketName, keyName, partNumber, true)); + } + + private OmKeyInfo getS3PartOmKeyInfo(String bucketName, String keyName, + int partNumber, boolean isHeadOp) throws IOException { OmKeyInfo keyInfo; if (omVersion.compareTo(OzoneManagerVersion.S3_PART_AWARE_GET) >= 0) { - keyInfo = getS3PartKeyInfo(bucketName, keyName, partNumber); + keyInfo = getS3PartKeyInfo(bucketName, keyName, partNumber, isHeadOp); } else { - keyInfo = getS3KeyInfo(bucketName, keyName, false); + keyInfo = getS3KeyInfo(bucketName, keyName, isHeadOp); List filteredKeyLocationInfo = keyInfo .getLatestVersionLocations().getBlocksLatestVersionOnly().stream() .filter(omKeyLocationInfo -> omKeyLocationInfo.getPartNumber() == @@ -1879,7 +1892,7 @@ public OzoneKeyDetails getS3KeyDetails(String bucketName, String keyName, .mapToLong(OmKeyLocationInfo::getLength) .sum()); } - return getOzoneKeyDetails(keyInfo); + return keyInfo; } @Nonnull @@ -1907,7 +1920,8 @@ private OmKeyInfo getS3KeyInfo( @Nonnull private OmKeyInfo getS3PartKeyInfo( - String bucketName, String keyName, int partNumber) throws IOException { + String bucketName, String keyName, int partNumber, boolean isHeadOp) + throws IOException { verifyBucketName(bucketName); Objects.requireNonNull(keyName, "keyName == null"); @@ -1921,6 +1935,7 @@ private OmKeyInfo getS3PartKeyInfo( .setLatestVersionLocation(getLatestVersionLocation) .setForceUpdateContainerCacheFromSCM(false) .setMultipartUploadPartNumber(partNumber) + .setHeadOp(isHeadOp) .build(); KeyInfoWithVolumeContext keyInfoWithS3Context = ozoneManagerClient.getKeyInfo(keyArgs, true); diff --git a/hadoop-ozone/dist/src/main/smoketest/s3/objectputget.robot b/hadoop-ozone/dist/src/main/smoketest/s3/objectputget.robot index 6cafa3513e9..417a8b49e15 100644 --- a/hadoop-ozone/dist/src/main/smoketest/s3/objectputget.robot +++ b/hadoop-ozone/dist/src/main/smoketest/s3/objectputget.robot @@ -283,9 +283,8 @@ Create&Download big file by multipart upload and get file via part numbers Should Be Equal As Integers 10000000 ${${part_1_size} + ${part_2_size}} - ${get_part_3_response} Execute AWSS3APICli get-object --bucket ${BUCKET} --key big_file /tmp/big_file_3 --part-number 3 - Should contain ${get_part_3_response} \"ContentLength\": 0 - Should contain ${get_part_3_response} \"PartsCount\": 2 + ${get_part_3_response} Execute AWSS3APICli and checkrc get-object --bucket ${BUCKET} --key big_file /tmp/big_file_3 --part-number 3 255 + Should contain ${get_part_3_response} InvalidPart # clean up Execute AWSS3Cli rm s3://${BUCKET}/big_file Execute rm -rf /tmp/big_file @@ -296,9 +295,8 @@ Create&Download big file by multipart upload and get file via part numbers Create&Download big file by multipart upload and get file not existed part number Execute head -c 10000000 /tmp/big_file ${result} Execute AWSS3CliDebug cp /tmp/big_file s3://${BUCKET}/ - ${get_part_99_response} Execute AWSS3APICli get-object --bucket ${BUCKET} --key big_file /tmp/big_file_1 --part-number 99 - Should contain ${get_part_99_response} \"ContentLength\": 0 - Should contain ${get_part_99_response} \"PartsCount\": 2 + ${get_part_99_response} Execute AWSS3APICli and checkrc get-object --bucket ${BUCKET} --key big_file /tmp/big_file_1 --part-number 99 255 + Should contain ${get_part_99_response} InvalidPart # clean up Execute AWSS3Cli rm s3://${BUCKET}/big_file Execute rm -rf /tmp/big_file diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java index c31e3154c32..b4832a21780 100644 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java +++ b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/AbstractS3SDKV1Tests.java @@ -1827,9 +1827,12 @@ public void testGetNotExistedPart(@TempDir Path tempDir) throws Exception { GetObjectRequest getObjectRequestOne = new GetObjectRequest(bucketName, keyName); getObjectRequestOne.setPartNumber(4); - S3Object s3ObjectOne = s3Client.getObject(getObjectRequestOne); - long partOneContentLength = s3ObjectOne.getObjectMetadata().getContentLength(); - assertEquals(0, partOneContentLength); + // Reading a part number beyond the object's part count must fail with + // InvalidPart, instead of returning an empty (0-byte) object. + AmazonServiceException ase = assertThrows(AmazonServiceException.class, + () -> s3Client.getObject(getObjectRequestOne)); + assertEquals(400, ase.getStatusCode()); + assertEquals("InvalidPart", ase.getErrorCode()); } @Test diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java index 90c850abff5..d14cde68146 100644 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java +++ b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java @@ -1438,6 +1438,60 @@ public void testLowLevelMultipartUpload(@TempDir Path tempDir) throws Exception assertEquals(userMetadata, headObjectResponse.metadata()); } + @Test + public void testGetNotExistedPart(@TempDir Path tempDir) throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + + s3Client.createBucket(b -> b.bucket(bucketName)); + + File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); + + createFile(multipartUploadFile, (int) (15 * MB)); + + multipartUpload(bucketName, keyName, multipartUploadFile, (int) (5 * MB), new HashMap<>(), Collections.emptyList()); + + // Reading a part number beyond the object's part count must fail with + // InvalidPart, instead of returning an empty (0-byte) object. + S3Exception exception = assertThrows(S3Exception.class, () -> s3Client.getObject(b -> b + .bucket(bucketName) + .key(keyName) + .partNumber(4))); + assertEquals(400, exception.statusCode()); + assertEquals("InvalidPart", exception.awsErrorDetails().errorCode()); + } + + @Test + public void testHeadObjectPartNumber(@TempDir Path tempDir) throws Exception { + final String bucketName = getBucketName(); + final String keyName = getKeyName(); + + s3Client.createBucket(b -> b.bucket(bucketName)); + + File multipartUploadFile = Files.createFile(tempDir.resolve("multipartupload.txt")).toFile(); + + createFile(multipartUploadFile, (int) (15 * MB)); + + multipartUpload(bucketName, keyName, multipartUploadFile, (int) (5 * MB), new HashMap<>(), Collections.emptyList()); + + // HEAD with a valid part number returns that part's metadata, including the + // total number of parts. + HeadObjectResponse headObjectResponse = s3Client.headObject(b -> b + .bucket(bucketName) + .key(keyName) + .partNumber(1)); + assertEquals(3, headObjectResponse.partsCount()); + + // HEAD with a part number beyond the object's part count must fail with + // HTTP 400, instead of returning whole-object metadata. HEAD has no + // response body, so only the status code is available to the client. + S3Exception exception = assertThrows(S3Exception.class, () -> s3Client.headObject(b -> b + .bucket(bucketName) + .key(keyName) + .partNumber(4))); + assertEquals(400, exception.statusCode()); + } + @Test public void testHeadObjectReturnsTaggingCount() { final String bucketName = getBucketName(); diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneClientMultipartUploadWithFSO.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneClientMultipartUploadWithFSO.java index 548c0acf368..53ff4c1b9f5 100644 --- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneClientMultipartUploadWithFSO.java +++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneClientMultipartUploadWithFSO.java @@ -1016,10 +1016,69 @@ void testGetNotExistedPart() throws Exception { s3Bucket.completeMultipartUpload(keyName, uploadID, partsMap); - OzoneKeyDetails s3KeyDetailsWithNotExistedParts = ozClient.getProxy() - .getS3KeyDetails(s3Bucket.getName(), keyName, 4); - List ozoneKeyLocations = s3KeyDetailsWithNotExistedParts.getOzoneKeyLocations(); - assertEquals(0, ozoneKeyLocations.size()); + // Reading a part number beyond the object's part count must fail with + // InvalidPart, instead of silently returning an empty (0-byte) result. + OzoneTestUtils.expectOmException(OMException.ResultCodes.INVALID_PART, () -> + ozClient.getProxy().getS3KeyDetails(s3Bucket.getName(), keyName, 4)); + } + + @Test + void testGetPartNumberWithNonContiguousParts() throws Exception { + String parentDir = "a/b/c/d/e/f/"; + keyName = parentDir + "file-ABC"; + OzoneVolume s3volume = store.getVolume("s3v"); + s3volume.createBucket(bucketName); + OzoneBucket s3Bucket = s3volume.getBucket(bucketName); + + Map partsMap = new TreeMap<>(); + String uploadID = initiateMultipartUpload(s3Bucket, keyName, RATIS, + ONE); + Pair partNameAndETag1 = uploadPart(s3Bucket, keyName, + uploadID, 1, generateData(OzoneConsts.OM_MULTIPART_MIN_SIZE, (byte) 97)); + partsMap.put(1, partNameAndETag1.getKey()); + + // Part 2 is uploaded but deliberately omitted from the completion below. + uploadPart(s3Bucket, keyName, uploadID, 2, + generateData(OzoneConsts.OM_MULTIPART_MIN_SIZE, (byte) 98)); + + byte[] part3Data = generateData(OzoneConsts.OM_MULTIPART_MIN_SIZE, (byte) 99); + Pair partNameAndETag3 = uploadPart(s3Bucket, keyName, + uploadID, 3, part3Data); + // Complete with non-contiguous part numbers {1, 3}, omitting part 2. + partsMap.put(3, partNameAndETag3.getKey()); + + s3Bucket.completeMultipartUpload(keyName, uploadID, partsMap); + + // Part 3 exists among the object's blocks, so reading it must succeed. + OzoneKeyDetails part3 = + ozClient.getProxy().getS3KeyDetails(s3Bucket.getName(), keyName, 3); + assertEquals(part3Data.length, part3.getDataSize()); + + // Part 2 was omitted at completion, so it does not exist and must fail. + OzoneTestUtils.expectOmException(OMException.ResultCodes.INVALID_PART, () -> + ozClient.getProxy().getS3KeyDetails(s3Bucket.getName(), keyName, 2)); + } + + @Test + void testGetPartNumberOnNonMultipartKey() throws Exception { + keyName = "non-multipart-file"; + OzoneVolume s3volume = store.getVolume("s3v"); + s3volume.createBucket(bucketName); + OzoneBucket s3Bucket = s3volume.getBucket(bucketName); + + byte[] data = generateData(1024, (byte) 97); + try (OzoneOutputStream out = s3Bucket.createKey(keyName, data.length)) { + out.write(data); + } + + // partNumber == 1 on a non-multipart key returns the whole object. + OzoneKeyDetails part1 = + ozClient.getProxy().getS3KeyDetails(bucketName, keyName, 1); + assertEquals(data.length, part1.getDataSize()); + + // partNumber > 1 on a non-multipart key is out of range. + OzoneTestUtils.expectOmException(OMException.ResultCodes.INVALID_PART, () -> + ozClient.getProxy().getS3KeyDetails(bucketName, keyName, 2)); } private String verifyUploadedPart(String uploadID, String partName, diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerImpl.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerImpl.java index dcdb3828ae6..5a526cb3c5a 100644 --- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerImpl.java +++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/TestKeyManagerImpl.java @@ -1523,13 +1523,11 @@ void testGetNotExistedPart() throws IOException { .setKeyName(keyName) .setMultipartUploadPartNumber(99) .build(); - OmKeyInfo omKeyInfo = keyManager.getKeyInfo(keyArgs, RESOLVED_BUCKET, "test"); - assertEquals(keyName, omKeyInfo.getKeyName()); - assertNotNull(omKeyInfo.getLatestVersionLocations()); - - List locationList = omKeyInfo.getLatestVersionLocations().getLocationList(); - assertNotNull(locationList); - assertEquals(0, locationList.size()); + // Reading a part number beyond the object's part count must fail with + // InvalidPart, instead of returning an empty (0-byte) result. + OMException ex = assertThrows(OMException.class, + () -> keyManager.getKeyInfo(keyArgs, RESOLVED_BUCKET, "test")); + assertEquals(OMException.ResultCodes.INVALID_PART, ex.getResult()); } private OmKeyInfo getMockedOmKeyInfo(OmBucketInfo bucketInfo, long parentId, String key, long objectId) { diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java index 08b6d6abbf1..0100d0cfbf6 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java @@ -73,6 +73,7 @@ import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.FILE_NOT_FOUND; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_KMS_PROVIDER; +import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_PART; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_NOT_FOUND; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.SCM_GET_PIPELINE_EXCEPTION; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.VOLUME_NOT_FOUND; @@ -640,12 +641,26 @@ private OmKeyInfo readKeyInfo(OmKeyArgs args, BucketLayout bucketLayout) .filter(it -> it.getPartNumber() == partNumberParam) .collect(Collectors.toList()); + // A requested part number that has no blocks does not exist in this + // object (part numbers may be non-contiguous), so it is out of range. + if (currentLocations.isEmpty()) { + throw new OMException("Cannot read part " + partNumberParam + + " of key " + keyName + " because it does not exist", + INVALID_PART); + } + value.updateLocationInfoList(currentLocations, true, true); value.setDataSize(currentLocations.stream() .mapToLong(BlockLocationInfo::getLength) .sum()); + } else if (partNumberParam > 1) { + // Non-multipart key: only part number 1 (the whole object) is valid; + // any higher part number is out of range. + throw new OMException("Cannot read part " + partNumberParam + + " of non-multipart key " + keyName, INVALID_PART); } + // Non-multipart key with partNumber == 1 returns the whole object. } return value; } diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java index 4e3b449bf0d..bffd8c8840d 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java @@ -372,6 +372,10 @@ Response handleGetRequest(ObjectRequestContext context, String keyPath) throws IOException, OS3Exception { final int partNumber = queryParams().getInt(QueryParams.PART_NUMBER, 0); + // A negative part number is not a valid part; reject it as InvalidArgument. + if (partNumber < 0) { + throw newError(INVALID_ARGUMENT, String.valueOf(partNumber)); + } final long startNanos = context.getStartNanos(); final PerformanceStringBuilder perf = context.getPerf(); @@ -562,6 +566,11 @@ public Response head( @PathParam(PATH) String keyPath) throws IOException, OS3Exception { long startNanos = Time.monotonicNowNanos(); S3GAction s3GAction = S3GAction.HEAD_KEY; + final int partNumber = queryParams().getInt(QueryParams.PART_NUMBER, 0); + // A negative part number is not a valid part; reject it as InvalidArgument. + if (partNumber < 0) { + throw newError(INVALID_ARGUMENT, String.valueOf(partNumber)); + } OzoneKey key; try { @@ -569,7 +578,11 @@ public Response head( OzoneBucket bucket = getVolume().getBucket(bucketName); S3Owner.verifyBucketOwnerCondition(getHeaders(), bucketName, bucket.getOwner()); } - key = getClientProtocol().headS3Object(bucketName, keyPath); + // A partNumber is validated against the object's parts and yields the + // metadata of that part; an out-of-range part throws InvalidPart. + key = (partNumber != 0) ? + getClientProtocol().headS3Object(bucketName, keyPath, partNumber) : + getClientProtocol().headS3Object(bucketName, keyPath); isFile(keyPath, key); Response conditionalResponse = S3ConditionalRequest.evaluatePreconditions( diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java index 8cc421c5c6e..e8cf6761757 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java @@ -120,6 +120,16 @@ public OzoneKey headS3Object(String bucketName, String keyName) .headObject(keyName); } + @Override + public OzoneKey headS3Object(String bucketName, String keyName, + int partNumber) throws IOException { + // The stub does not model individual multipart parts. Fail fast rather than + // silently returning whole-object metadata, so a test cannot get a false + // positive for an invalid part. + throw new UnsupportedOperationException( + "headS3Object with a part number is not supported by the stub"); + } + @Override public OzoneKeyDetails getS3KeyDetails(String bucketName, String keyName) throws IOException { diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectGet.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectGet.java index 54d28c08b8b..0494664cd40 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectGet.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectGet.java @@ -22,6 +22,7 @@ import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds; import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.get; import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.NO_SUCH_KEY; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.PRECOND_FAILED; import static org.apache.hadoop.ozone.s3.util.S3Consts.IF_MATCH_HEADER; @@ -55,6 +56,7 @@ import org.apache.hadoop.ozone.s3.HeaderPreprocessor; import org.apache.hadoop.ozone.s3.exception.OS3Exception; import org.apache.hadoop.ozone.s3.util.RFC1123Util; +import org.apache.hadoop.ozone.s3.util.S3Consts; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -107,6 +109,13 @@ public void init() throws OS3Exception, IOException { assertSucceeds(() -> put(rest, BUCKET_NAME, KEY_WITH_TAG, CONTENT)); } + @Test + public void testGetWithNegativePartNumber() throws Exception { + rest.queryParamsForTest().setInt(S3Consts.QueryParams.PART_NUMBER, -1); + assertErrorResponse(INVALID_ARGUMENT, + () -> get(rest, BUCKET_NAME, KEY_NAME)); + } + @Test public void testGet() throws IOException, OS3Exception { //WHEN diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectHead.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectHead.java index 158d856f7d4..ccce932d0d7 100644 --- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectHead.java +++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectHead.java @@ -23,6 +23,7 @@ import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertStatus; import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertSucceeds; import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put; +import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.INVALID_ARGUMENT; import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.PRECOND_FAILED; import static org.apache.hadoop.ozone.s3.util.S3Consts.CUSTOM_METADATA_HEADER_PREFIX; import static org.apache.hadoop.ozone.s3.util.S3Consts.IF_MATCH_HEADER; @@ -55,6 +56,7 @@ import org.apache.hadoop.ozone.s3.HeaderPreprocessor; import org.apache.hadoop.ozone.s3.exception.OS3Exception; import org.apache.hadoop.ozone.s3.util.RFC1123Util; +import org.apache.hadoop.ozone.s3.util.S3Consts; import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,6 +110,14 @@ public void testHeadFailByBadName() throws Exception { assertStatus(HttpStatus.SC_NOT_FOUND, () -> keyEndpoint.head(bucketName, "badKeyName")); } + @Test + public void testHeadWithNegativePartNumber() throws Exception { + keyEndpoint.queryParamsForTest() + .setInt(S3Consts.QueryParams.PART_NUMBER, -1); + assertErrorResponse(INVALID_ARGUMENT, + () -> keyEndpoint.head(bucketName, "key1")); + } + @Test public void testHeadIfMatch() throws Exception { assertSucceeds(() -> put(keyEndpoint, bucketName, "etag-key", "head-content"));