[ 
https://issues.apache.org/jira/browse/HDDS-17630?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Ritesh Shukla updated HDDS-17630:
---------------------------------
    Description: 
h3. Problem

An S3 {{PUT}} on an object key means one of two things:
* {{PUT /bucket/key?partNumber=N&uploadId=<id>}} uploads one part of a 
multipart upload (UploadPart). Nothing under the key changes until the upload 
is completed.
* {{PUT /bucket/key}} writes the object (PutObject).

When {{uploadId}} is empty, or {{partNumber}} comes with no {{uploadId}} at 
all, the S3 Gateway handles the request as a PutObject, or as a CopyObject when 
{{x-amz-copy-source}} is set. The body (or the copy source) replaces the 
committed object, and the client gets {{200 OK}}, just as for a part upload:

{code}
PUT /bucket/key                          body "original object"  -> 200, key 
holds "original object"
PUT /bucket/key?partNumber=1&uploadId=   body "part 1"           -> 200, key 
now holds "part 1"
{code}

AWS S3 refuses each of these requests and leaves the object alone (tested 
against AWS S3 on 2026-10-31). The Ozone column was run on master (ef63bacef7) 
against a mini cluster with the AWS SDK for Java v2, and in the S3 Gateway 
unit-test harness:

|| Request || AWS S3 || S3 Gateway on master ||
| {{PUT ?partNumber=1&uploadId=}} | 400 InvalidArgument, object kept | 200, 
object replaced by the body |
| {{PUT ?partNumber=1&uploadId}} (no {{=}}) | 400 InvalidArgument, object kept 
| 200, object replaced by the body: the gateway reads a bare {{uploadId}} as 
the same empty value |
| {{PUT ?partNumber=1}} (no {{uploadId}}) | 400 InvalidArgument, object kept | 
200, object replaced by the body |
| {{PUT ?partNumber=1&uploadId=}} with {{x-amz-copy-source}} (UploadPartCopy) | 
400 InvalidArgument, object kept | 200, object replaced by a full copy of the 
source; {{x-amz-copy-source-range}} is ignored |
| {{PUT ?uploadId=}} (no {{partNumber}}) | 405 MethodNotAllowed, object kept | 
200, object replaced by the body |
| {{PUT ?partNumber=}} or {{?partNumber=0}} (no {{uploadId}}), or 
{{?partNumber=&uploadId=}} | 400 InvalidArgument, object kept 
({{?partNumber=abc}} too) | 200, object replaced by the body |
| {{PUT ?partNumber=1&uploadId=}} while an upload is in progress on the key | 
400 InvalidArgument, object kept, no part added | 200, object replaced by the 
body |
| {{PUT ?uploadId=<non-empty ID>}} (no {{partNumber}}), unknown or real upload 
| 405 MethodNotAllowed, object kept, no part added | 400 InvalidArgument "Part 
number must be an integer between 1 and 10000, inclusive", object kept, no part 
added: the gateway reads the missing part number as 0, and Ozone's client 
library in the gateway refuses it before calling OM |

S3's InvalidArgument message is "This operation does not accept partNumber 
without uploadId", for any value of {{partNumber}}. With an empty body, the 
gateway truncates the object to 0 bytes (run).

On master, the server-side encryption check added by HDDS-17498 runs before the 
gateway picks the operation. So a request that also asks for encryption the 
gateway cannot honour (SSE-C in any bucket, or SSE-S3 or SSE-KMS on an 
unencrypted bucket) is refused first with 501 NotImplemented and keeps the 
object (run for SSE-C and SSE-S3 on an unencrypted bucket, the other cases read 
from code). Without an encryption header, or with one an encrypted bucket can 
honour, the object is replaced as above (run).

Other handlers that read {{uploadId}} (gateway answers run against a mini 
cluster):
* {{POST ?uploadId=}} (CompleteMultipartUpload) and {{GET ?uploadId=}} 
(ListParts): S3 and the gateway both answer 404 NoSuchUpload.
* {{DELETE ?uploadId=}}: S3 answers 404 NoSuchUpload; the gateway deletes the 
object. That is HDDS-17599.
* {{HEAD ?uploadId=}} and {{GET ?partNumber=1&uploadId=}}: S3 answers 405 
MethodNotAllowed; the gateway answers 200 (HeadObject) and 404 NoSuchUpload. 
Both are read-only and are left out of this fix.

h3. Impact

* Only a caller who may already write the key can do this. The operation that 
actually runs is authorized as itself: PutObject, or GetObject on the source 
and PutObject on the destination for the copy form. No permission boundary is 
crossed (read from code).
* What is lost: the object's data, metadata and tags (run). The response is a 
success, so the client cannot tell. A client that uploads several parts under 
the same empty ID ends up with only the last part as the object (run).
* How a client gets here: an application bug that passes an empty upload ID, 
for example after losing the CreateMultipartUpload response. Stock clients send 
it as-is. The AWS SDK for Java v2 (2.55.6) and v1 (1.12.788), and the AWS CLI 
v2 (2.35.10), all put {{uploadId=}} on the wire for an empty ID, including for 
UploadPartCopy; both SDKs refuse a null ID before sending (run against a local 
capture server).

h3. Cause

{{ObjectEndpoint#handlePutRequest}} treats a request as a part upload only when 
{{uploadID != null && !uploadID.equals("")}} (ObjectEndpoint.java:216 at 
ef63bacef7). Every other request becomes a PutObject, or a CopyObject when 
{{x-amz-copy-source}} is set (ObjectEndpoint.java:238-245). That path never 
reads {{partNumber}}. The only checks that run before this condition are the 
bucket lookup and the server-side encryption check 
(ObjectEndpoint.java:210-212).

The empty-string test dates from the first UploadPart code (HDDS-902). The 
parameter was then declared with {{@DefaultValue("")}}, so a missing 
{{uploadId}} arrived as an empty string, and comparing with {{""}} was how 
"absent" was written. HDDS-14209 switched to reading query parameters directly, 
where a missing {{uploadId}} is {{null}}, but kept the comparison. So a present 
but empty {{uploadId}} still counts as absent.

h3. Proposed fix

Answer as S3 does (tested against AWS S3 on 2026-10-31), before any OM call:
* {{partNumber}} present and {{uploadId}} missing or empty: 400 
{{InvalidArgument}} with S3's message, "This operation does not accept 
partNumber without uploadId". This also covers UploadPartCopy.
* {{uploadId}} present, empty or not, and no {{partNumber}}: 405 
{{MethodNotAllowed}}, as S3 answers both. {{S3ErrorTable}} gains a 
{{METHOD_NOT_ALLOWED}} entry with S3's code and message.
* Everything else is unchanged: a non-empty {{uploadId}} with a {{partNumber}} 
is still a part upload, and a PUT with neither parameter is still a PutObject.

Both checks go at the top of {{handlePutRequest}}, before the bucket lookup and 
the server-side encryption check, so a refused request makes no OM call and the 
key is never opened. It is audited and counted as a failed PutObject. The 
part-upload branch then only needs {{uploadID != null}}.

The obvious one-line fix, dropping {{!uploadID.equals("")}} so the empty ID 
reaches OM, is worse. Run against a mini cluster, it answers the empty ID with 
OM's 404 NoSuchUpload, not S3's 400, and {{PUT ?partNumber=1}} with no 
{{uploadId}} still overwrites the object. UploadPartCopy would also read the 
source key before failing (read from code). HDDS-17599 answers DELETE with 
NoSuchUpload because that is what S3 does for DELETE; S3 answers PUT 
differently, so this fix does too.

h3. Testing

* Unit, {{TestPartUpload}}: a PUT with {{partNumber}} and no non-empty 
{{uploadId}} ({{?partNumber=1&uploadId=}}, {{?partNumber=1}}, {{?partNumber=}}, 
{{?partNumber=0}}, {{?partNumber=abc}}, {{?partNumber=&uploadId=}}) expects 
S3's 400; a PUT with {{uploadId}} and no {{partNumber}} ({{?uploadId=}}, 
{{?uploadId=<unknown>}}) expects S3's 405; and with an upload in progress on 
the key, {{?partNumber=1&uploadId=}} (400) and {{?uploadId=<that upload>}} 
(405) must add no part. Each test puts a key first and checks that its content 
is unchanged. They run with and without data stream. On master all 20 runs 
fail: 16 replace the key, 2 answer 404 instead of 405, and 2 are accepted by 
the unit tests' client stub, which does not check part numbers.
* Unit, {{TestMultipartUploadWithCopy}}: UploadPartCopy with 
{{?partNumber=1&uploadId=}} and with {{?partNumber=1}}. On master both copy the 
source over the destination.
* {{EndpointTestUtils#put}} stops adding {{partNumber=0}} to plain PUTs and 
clears a left-over {{uploadId}}. A real PutObject carries neither, and the new 
checks would refuse it: with the fix but without this change, 275 existing 
unit-test runs in 17 classes fail.
* Integration, {{AbstractS3SDKV2Tests}}: {{uploadPart}} and {{uploadPartCopy}} 
with {{uploadId("")}}, which the SDK sends as {{uploadId=}}. Each expects 400 
InvalidArgument with S3's message and the object's content unchanged. On master 
the object is replaced.
* Run on ef63bacef7 with the change: all 1,943 s3gateway unit-test runs pass, 
and so do the 255 tests of the SDK v1 and v2 integration suite ({{TestS3SDK}}). 
Without the change, only the new tests fail.

----
🤖 Generated with [Claude Code|https://claude.com/claude-code]

  was:
h3. Problem

An S3 {{PUT}} on an object key means one of two things:
* {{PUT /bucket/key?partNumber=N&uploadId=<id>}} uploads one part of a 
multipart upload (UploadPart). Nothing under the key changes until the upload 
is completed.
* {{PUT /bucket/key}} writes the object (PutObject).

When {{uploadId}} is empty, or {{partNumber}} comes with no {{uploadId}} at 
all, the S3 Gateway handles the request as a PutObject, or as a CopyObject when 
{{x-amz-copy-source}} is set. The body (or the copy source) replaces the 
committed object, and the client gets {{200 OK}}, just as for a part upload:

{code}
PUT /bucket/key                          body "original object"  -> 200, key 
holds "original object"
PUT /bucket/key?partNumber=1&uploadId=   body "part 1"           -> 200, key 
now holds "part 1"
{code}

AWS S3 refuses each of these requests and leaves the object alone (tested 
against AWS S3 on 2026-10-31). The Ozone column was run in the S3 Gateway 
unit-test harness on master:

|| Request || AWS S3 || S3 Gateway on master ||
| {{PUT ?partNumber=1&uploadId=}} | 400 InvalidArgument, object kept | 200, 
object replaced by the body |
| {{PUT ?partNumber=1&uploadId}} (no {{=}}) | 400 InvalidArgument, object kept 
| same as above: the gateway's JAX-RS layer decodes a bare {{uploadId}} to the 
same empty value (checked with Jersey's query decoder) |
| {{PUT ?partNumber=1}} (no {{uploadId}}) | 400 InvalidArgument, object kept | 
200, object replaced by the body |
| {{PUT ?partNumber=1&uploadId=}} with {{x-amz-copy-source}} (UploadPartCopy) | 
400 InvalidArgument, object kept | 200, object replaced by a full copy of the 
source; {{x-amz-copy-source-range}} is ignored |
| {{PUT ?uploadId=}} (no {{partNumber}}) | 405 MethodNotAllowed, object kept | 
200, object replaced by the body |
| {{PUT ?partNumber=}} or {{?partNumber=0}} (no {{uploadId}}), or 
{{?partNumber=&uploadId=}} | 400 InvalidArgument, object kept 
({{?partNumber=abc}} too) | 200, object replaced by the body |
| {{PUT ?partNumber=1&uploadId=}} while an upload is in progress on the key | 
400 InvalidArgument, object kept, no part added | 200, object replaced by the 
body |
| {{PUT ?uploadId=<non-empty ID>}} (no {{partNumber}}), unknown or real upload 
| 405 MethodNotAllowed, object kept, no part added | unknown ID: 404 
NoSuchUpload; real upload: OM refuses part number 0 with 400 (read from code) |

S3's InvalidArgument message is "This operation does not accept partNumber 
without uploadId", for any value of {{partNumber}}. With an empty body, the 
gateway truncates the object to 0 bytes.

Other handlers that read {{uploadId}}:
* {{POST ?uploadId=}} (CompleteMultipartUpload) and {{GET ?uploadId=}} 
(ListParts): S3 and the gateway both answer 404 NoSuchUpload. On the gateway 
side this was run against the test client; that OM refuses an empty upload ID 
was read from code.
* {{DELETE ?uploadId=}}: S3 answers 404 NoSuchUpload; the gateway deletes the 
object. That is HDDS-17599.
* {{HEAD ?uploadId=}} and {{GET ?partNumber=1&uploadId=}}: S3 answers 405 
MethodNotAllowed; the gateway answers 200 (HeadObject) and 404 NoSuchUpload. 
Both are read-only and are left out of this fix.

h3. Impact

* Only a caller who may already write the key can do this. The operation that 
actually runs is authorized as itself: PutObject, or GetObject on the source 
and PutObject on the destination for the copy form. No permission boundary is 
crossed (read from code).
* What is lost: the object's data (run), and with it the object's metadata and 
tags (read from code). The response is a success, so the client cannot tell. A 
client that uploads several parts under the same empty ID ends up with only the 
last part as the object (inferred).
* How a client gets here: an application bug that passes an empty upload ID, 
for example after losing the CreateMultipartUpload response. Stock clients send 
it as-is. The AWS SDK for Java v2 (2.55.6) and v1 (1.12.788), and the AWS CLI 
v2 (2.35.10), all put {{uploadId=}} on the wire for an empty ID, including for 
UploadPartCopy; both SDKs refuse a null ID before sending (run against a local 
capture server).

h3. Cause

{{ObjectEndpoint#handlePutRequest}} treats a request as a part upload only when 
{{uploadID != null && !uploadID.equals("")}} (ObjectEndpoint.java:214 on 
master). Every other request becomes a PutObject, or a CopyObject when 
{{x-amz-copy-source}} is set (ObjectEndpoint.java:236-243). That path never 
reads {{partNumber}}.

The empty-string test dates from the first UploadPart code (HDDS-902). The 
parameter was then declared with {{@DefaultValue("")}}, so a missing 
{{uploadId}} arrived as an empty string, and comparing with {{""}} was how 
"absent" was written. HDDS-14209 switched to reading query parameters directly, 
where a missing {{uploadId}} is {{null}}, but kept the comparison. So a present 
but empty {{uploadId}} still counts as absent.

h3. Proposed fix

Answer as S3 does (tested against AWS S3 on 2026-10-31), before any OM call:
* {{partNumber}} present and {{uploadId}} missing or empty: 400 
{{InvalidArgument}} with S3's message, "This operation does not accept 
partNumber without uploadId". This also covers UploadPartCopy.
* {{uploadId}} present, empty or not, and no {{partNumber}}: 405 
{{MethodNotAllowed}}, as S3 answers both. {{S3ErrorTable}} gains a 
{{METHOD_NOT_ALLOWED}} entry with S3's code and message.
* Everything else is unchanged: a non-empty {{uploadId}} with a {{partNumber}} 
is still a part upload, and a PUT with neither parameter is still a PutObject.

Both checks go at the top of {{handlePutRequest}}. A refused request is audited 
and counted as a failed PutObject, and the key is never opened. The part-upload 
branch then only needs {{uploadID != null}}.

The obvious one-line fix, dropping {{!uploadID.equals("")}} so the empty ID 
reaches OM, is worse. OM would answer 404 NoSuchUpload, not S3's 400. {{PUT 
?partNumber=1}} with no {{uploadId}} would still overwrite the object. And 
UploadPartCopy would read the source key before failing. HDDS-17599 answers 
DELETE with NoSuchUpload because that is what S3 does for DELETE; S3 answers 
PUT differently, so this fix does too.

h3. Testing

* Unit, {{TestPartUpload}}: one parameterized case for each of 
{{?partNumber=1&uploadId=}}, {{?partNumber=1}} and {{?uploadId=}}. Each puts a 
key, sends the PUT, checks S3's error code and message, and checks that the 
key's content and size are unchanged. It runs with and without data stream. On 
master all six runs fail: the PUT succeeds and the key is replaced. To add with 
the fix: {{?partNumber=}}, {{?partNumber=0}} and {{?partNumber=abc}} without an 
upload ID (400), and {{?uploadId=<non-empty>}} without a part number (405).
* Unit, {{TestMultipartUploadWithCopy}}: the same for UploadPartCopy with 
{{?partNumber=1&uploadId=}}. On master the destination is replaced by the 
source.
* {{EndpointTestUtils#put}} stops adding {{partNumber=0}} to plain PUTs. A real 
PutObject carries no {{partNumber}}, and the new check would refuse it.
* All s3gateway unit tests pass with the change.
* Integration (to add with the fix): a case in {{AbstractS3SDKV2Tests}} calls 
{{uploadPart}} and {{uploadPartCopy}} with {{uploadId("")}}, which the SDK 
sends as {{uploadId=}}. It expects 400 InvalidArgument and the same ETag from 
{{headObject}} afterwards.

----
🤖 Generated with [Claude Code|https://claude.com/claude-code]


> S3G: a part upload with an empty or missing uploadId overwrites the object 
> instead of failing
> ---------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17630
>                 URL: https://issues.apache.org/jira/browse/HDDS-17630
>             Project: Apache Ozone
>          Issue Type: Bug
>          Components: S3
>            Reporter: Ritesh Shukla
>            Assignee: Ritesh Shukla
>            Priority: Major
>
> h3. Problem
> An S3 {{PUT}} on an object key means one of two things:
> * {{PUT /bucket/key?partNumber=N&uploadId=<id>}} uploads one part of a 
> multipart upload (UploadPart). Nothing under the key changes until the upload 
> is completed.
> * {{PUT /bucket/key}} writes the object (PutObject).
> When {{uploadId}} is empty, or {{partNumber}} comes with no {{uploadId}} at 
> all, the S3 Gateway handles the request as a PutObject, or as a CopyObject 
> when {{x-amz-copy-source}} is set. The body (or the copy source) replaces the 
> committed object, and the client gets {{200 OK}}, just as for a part upload:
> {code}
> PUT /bucket/key                          body "original object"  -> 200, key 
> holds "original object"
> PUT /bucket/key?partNumber=1&uploadId=   body "part 1"           -> 200, key 
> now holds "part 1"
> {code}
> AWS S3 refuses each of these requests and leaves the object alone (tested 
> against AWS S3 on 2026-10-31). The Ozone column was run on master 
> (ef63bacef7) against a mini cluster with the AWS SDK for Java v2, and in the 
> S3 Gateway unit-test harness:
> || Request || AWS S3 || S3 Gateway on master ||
> | {{PUT ?partNumber=1&uploadId=}} | 400 InvalidArgument, object kept | 200, 
> object replaced by the body |
> | {{PUT ?partNumber=1&uploadId}} (no {{=}}) | 400 InvalidArgument, object 
> kept | 200, object replaced by the body: the gateway reads a bare 
> {{uploadId}} as the same empty value |
> | {{PUT ?partNumber=1}} (no {{uploadId}}) | 400 InvalidArgument, object kept 
> | 200, object replaced by the body |
> | {{PUT ?partNumber=1&uploadId=}} with {{x-amz-copy-source}} (UploadPartCopy) 
> | 400 InvalidArgument, object kept | 200, object replaced by a full copy of 
> the source; {{x-amz-copy-source-range}} is ignored |
> | {{PUT ?uploadId=}} (no {{partNumber}}) | 405 MethodNotAllowed, object kept 
> | 200, object replaced by the body |
> | {{PUT ?partNumber=}} or {{?partNumber=0}} (no {{uploadId}}), or 
> {{?partNumber=&uploadId=}} | 400 InvalidArgument, object kept 
> ({{?partNumber=abc}} too) | 200, object replaced by the body |
> | {{PUT ?partNumber=1&uploadId=}} while an upload is in progress on the key | 
> 400 InvalidArgument, object kept, no part added | 200, object replaced by the 
> body |
> | {{PUT ?uploadId=<non-empty ID>}} (no {{partNumber}}), unknown or real 
> upload | 405 MethodNotAllowed, object kept, no part added | 400 
> InvalidArgument "Part number must be an integer between 1 and 10000, 
> inclusive", object kept, no part added: the gateway reads the missing part 
> number as 0, and Ozone's client library in the gateway refuses it before 
> calling OM |
> S3's InvalidArgument message is "This operation does not accept partNumber 
> without uploadId", for any value of {{partNumber}}. With an empty body, the 
> gateway truncates the object to 0 bytes (run).
> On master, the server-side encryption check added by HDDS-17498 runs before 
> the gateway picks the operation. So a request that also asks for encryption 
> the gateway cannot honour (SSE-C in any bucket, or SSE-S3 or SSE-KMS on an 
> unencrypted bucket) is refused first with 501 NotImplemented and keeps the 
> object (run for SSE-C and SSE-S3 on an unencrypted bucket, the other cases 
> read from code). Without an encryption header, or with one an encrypted 
> bucket can honour, the object is replaced as above (run).
> Other handlers that read {{uploadId}} (gateway answers run against a mini 
> cluster):
> * {{POST ?uploadId=}} (CompleteMultipartUpload) and {{GET ?uploadId=}} 
> (ListParts): S3 and the gateway both answer 404 NoSuchUpload.
> * {{DELETE ?uploadId=}}: S3 answers 404 NoSuchUpload; the gateway deletes the 
> object. That is HDDS-17599.
> * {{HEAD ?uploadId=}} and {{GET ?partNumber=1&uploadId=}}: S3 answers 405 
> MethodNotAllowed; the gateway answers 200 (HeadObject) and 404 NoSuchUpload. 
> Both are read-only and are left out of this fix.
> h3. Impact
> * Only a caller who may already write the key can do this. The operation that 
> actually runs is authorized as itself: PutObject, or GetObject on the source 
> and PutObject on the destination for the copy form. No permission boundary is 
> crossed (read from code).
> * What is lost: the object's data, metadata and tags (run). The response is a 
> success, so the client cannot tell. A client that uploads several parts under 
> the same empty ID ends up with only the last part as the object (run).
> * How a client gets here: an application bug that passes an empty upload ID, 
> for example after losing the CreateMultipartUpload response. Stock clients 
> send it as-is. The AWS SDK for Java v2 (2.55.6) and v1 (1.12.788), and the 
> AWS CLI v2 (2.35.10), all put {{uploadId=}} on the wire for an empty ID, 
> including for UploadPartCopy; both SDKs refuse a null ID before sending (run 
> against a local capture server).
> h3. Cause
> {{ObjectEndpoint#handlePutRequest}} treats a request as a part upload only 
> when {{uploadID != null && !uploadID.equals("")}} (ObjectEndpoint.java:216 at 
> ef63bacef7). Every other request becomes a PutObject, or a CopyObject when 
> {{x-amz-copy-source}} is set (ObjectEndpoint.java:238-245). That path never 
> reads {{partNumber}}. The only checks that run before this condition are the 
> bucket lookup and the server-side encryption check 
> (ObjectEndpoint.java:210-212).
> The empty-string test dates from the first UploadPart code (HDDS-902). The 
> parameter was then declared with {{@DefaultValue("")}}, so a missing 
> {{uploadId}} arrived as an empty string, and comparing with {{""}} was how 
> "absent" was written. HDDS-14209 switched to reading query parameters 
> directly, where a missing {{uploadId}} is {{null}}, but kept the comparison. 
> So a present but empty {{uploadId}} still counts as absent.
> h3. Proposed fix
> Answer as S3 does (tested against AWS S3 on 2026-10-31), before any OM call:
> * {{partNumber}} present and {{uploadId}} missing or empty: 400 
> {{InvalidArgument}} with S3's message, "This operation does not accept 
> partNumber without uploadId". This also covers UploadPartCopy.
> * {{uploadId}} present, empty or not, and no {{partNumber}}: 405 
> {{MethodNotAllowed}}, as S3 answers both. {{S3ErrorTable}} gains a 
> {{METHOD_NOT_ALLOWED}} entry with S3's code and message.
> * Everything else is unchanged: a non-empty {{uploadId}} with a 
> {{partNumber}} is still a part upload, and a PUT with neither parameter is 
> still a PutObject.
> Both checks go at the top of {{handlePutRequest}}, before the bucket lookup 
> and the server-side encryption check, so a refused request makes no OM call 
> and the key is never opened. It is audited and counted as a failed PutObject. 
> The part-upload branch then only needs {{uploadID != null}}.
> The obvious one-line fix, dropping {{!uploadID.equals("")}} so the empty ID 
> reaches OM, is worse. Run against a mini cluster, it answers the empty ID 
> with OM's 404 NoSuchUpload, not S3's 400, and {{PUT ?partNumber=1}} with no 
> {{uploadId}} still overwrites the object. UploadPartCopy would also read the 
> source key before failing (read from code). HDDS-17599 answers DELETE with 
> NoSuchUpload because that is what S3 does for DELETE; S3 answers PUT 
> differently, so this fix does too.
> h3. Testing
> * Unit, {{TestPartUpload}}: a PUT with {{partNumber}} and no non-empty 
> {{uploadId}} ({{?partNumber=1&uploadId=}}, {{?partNumber=1}}, 
> {{?partNumber=}}, {{?partNumber=0}}, {{?partNumber=abc}}, 
> {{?partNumber=&uploadId=}}) expects S3's 400; a PUT with {{uploadId}} and no 
> {{partNumber}} ({{?uploadId=}}, {{?uploadId=<unknown>}}) expects S3's 405; 
> and with an upload in progress on the key, {{?partNumber=1&uploadId=}} (400) 
> and {{?uploadId=<that upload>}} (405) must add no part. Each test puts a key 
> first and checks that its content is unchanged. They run with and without 
> data stream. On master all 20 runs fail: 16 replace the key, 2 answer 404 
> instead of 405, and 2 are accepted by the unit tests' client stub, which does 
> not check part numbers.
> * Unit, {{TestMultipartUploadWithCopy}}: UploadPartCopy with 
> {{?partNumber=1&uploadId=}} and with {{?partNumber=1}}. On master both copy 
> the source over the destination.
> * {{EndpointTestUtils#put}} stops adding {{partNumber=0}} to plain PUTs and 
> clears a left-over {{uploadId}}. A real PutObject carries neither, and the 
> new checks would refuse it: with the fix but without this change, 275 
> existing unit-test runs in 17 classes fail.
> * Integration, {{AbstractS3SDKV2Tests}}: {{uploadPart}} and 
> {{uploadPartCopy}} with {{uploadId("")}}, which the SDK sends as 
> {{uploadId=}}. Each expects 400 InvalidArgument with S3's message and the 
> object's content unchanged. On master the object is replaced.
> * Run on ef63bacef7 with the change: all 1,943 s3gateway unit-test runs pass, 
> and so do the 255 tests of the SDK v1 and v2 integration suite 
> ({{TestS3SDK}}). Without the change, only the new tests fail.
> ----
> 🤖 Generated with [Claude Code|https://claude.com/claude-code]



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to