Skip to content

CLDSRV-898: handle checksums in CompleteMultipartUpload#6170

Open
leif-scality wants to merge 10 commits into
development/9.4from
improvement/CLDSRV-898-complete-mpu-checksums-2
Open

CLDSRV-898: handle checksums in CompleteMultipartUpload#6170
leif-scality wants to merge 10 commits into
development/9.4from
improvement/CLDSRV-898-complete-mpu-checksums-2

Conversation

@leif-scality
Copy link
Copy Markdown
Contributor

  • Calculate and compare the final object checksum with the one sent by the headers
  • Check that all parts have the correct checksum and checksum type
  • stores the final checksum when FULL_OBJECT (COMPOSITE are going to be stored by https://scality.atlassian.net/browse/S3C-10399)

@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

LGTM

Review by Claude Code

@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
9179 1 9178 0
View the top 1 failed test(s) by shortest run time
"before each" hook for "should retrieve a part put after part copied from MPU"::GET object With v4 signature With PartNumber field uploadPartCopy "before each" hook for "should retrieve a part put after part copied from MPU"
Stack Traces | 6.22s run time
Connection timed out after 5000 ms

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@leif-scality leif-scality force-pushed the improvement/CLDSRV-902-calculate-final-checksum-from-parts branch 2 times, most recently from 2493909 to 538c16c Compare May 21, 2026 09:18
Base automatically changed from improvement/CLDSRV-902-calculate-final-checksum-from-parts to development/9.4 May 21, 2026 13:18
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 21, 2026

Hello leif-scality,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 21, 2026

Incorrect fix version

The Fix Version/s in issue CLDSRV-898 contains:

  • None

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 9.4.0

Please check the Fix Version/s of CLDSRV-898, or the target
branch of this pull request.

@leif-scality leif-scality force-pushed the improvement/CLDSRV-898-complete-mpu-checksums-2 branch from 27b4a43 to a90bd94 Compare May 21, 2026 13:24
@claude
Copy link
Copy Markdown

claude Bot commented May 21, 2026

LGTM

Well-designed separation of CompleteMPU's final-object checksum semantics from body-digest validation. The callNext double-callback guard in processParts is a good defensive pattern for the mixed callback/promise flow. Test coverage is thorough across all algorithm/type combinations, including edge cases for default MPUs, legacy MPUs, and soft-null vs InternalError distinction.

Review by Claude Code

Comment thread lib/api/completeMultipartUpload.js Outdated
Comment on lines +73 to +75
const parts = jsonList.Part || [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const parts = jsonList.Part || [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
for (const part of (jsonList.Part || [])) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread lib/api/completeMultipartUpload.js Outdated
if (tag !== expectedTag) {
const algoLabel = tag.replace(/^Checksum/, '').toLowerCase();
return errorInstances.BadDigest.customizeDescription(
`The ${algoLabel} you specified for part ${partNumber} ` + 'did not match what we received.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`The ${algoLabel} you specified for part ${partNumber} ` + 'did not match what we received.',
`The ${algoLabel} you specified for part ${partNumber} did not match what we received.`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return errorInstances.InvalidRequest.customizeDescription(
`The upload was created using a ${mpuAlgo} checksum. ` +
'The complete request must include the checksum for each ' +
`part. It was missing for part ${partNumber} in the request.`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`part. It was missing for part ${partNumber} in the request.`,
`part, it is missing for part ${partNumber} in the request.`,

Copy link
Copy Markdown
Contributor Author

@leif-scality leif-scality May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current error message is identical to the one sent by AWS

<Error>
<Code>InvalidRequest</Code>
<Message>The upload was created using a sha256 checksum. The complete request must include the checksum for each part. It
  was missing for part 1 in the request.</Message>
<RequestId>2CMRHS7YQJ418XRD</RequestId><HostId>Z+nL7Rh7aglZj/H2D2aesp/j38QjuvN/wpgur86e7/0P6nohBelRVXaXY+mWSkQ
  dGaPIKHTmmg0iPUkaWjIWFTHmMJdNZolP</HostId>
</Error>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a test file multipartUpload.js, wouldn't it be better to add the tests there? Or if a refactor into multiple files is beneficial, maybe port some related tests from there to this new file to avoid scattering the complete MPU tests into multiple files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved the tests to multipartUpload.js. This also triggered the new prettier linter in the file, I added it as a separate commit

const DummyRequest = require('../DummyRequest');
const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers');

const SPLITTER = '..|..';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const SPLITTER = '..|..';
const SPLITTER = constants.splitter;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// XML element name AWS uses for each algorithm in CompleteMultipartUpload's
// per-part body.
const TAG_BY_ALGO = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be in constants too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The algorithms object contains the TAG of each algo already, this object is just for testing that the tag was not changed in the algorithms object

assert.strictEqual(
err.description,
'One or more of the specified parts could not be ' +
'found. The part may not have been uploaded, or ' +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the double space intentional? Also not very fond of matching against such a long error message that may vary, but that's okay I think.

Copy link
Copy Markdown
Contributor Author

@leif-scality leif-scality May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWS returns two spaces. For all the final API/XML errors I return exactly what AWS returns

 <Error><Code>InvalidPart</Code>
<Message>One or more of the specified parts could not be found.  The part may not have been uploaded, or the specified entity
  tag may not match the part's entity tag.</Message>
<UploadId>CTFNKyL2hI6n6irH0zbVWDzdPZ4n2ueJceRh1juCeuL2X5HOjrvCmXQMEqaoAatEWTEa3pWWxC7t9lOStMzjo0nJb4pv8Ct6oT
  Hv2n8mggVXRQ8RxiXyVyt3.3zpY98HVsZd.ozihhJ1HdUjLCkJtwJMQdNBd4fSdG9drS80vdg-</UploadId>
<PartNumber>1</PartNumber>
<ETag>0ebf9257a12e808d107b2ed1a826c122</ETag>
<RequestId>DAQAPVMCMSY4PDPV</RequestId>
<HostId>XS/2re3ieUQRTKdANLtZv14qyB2h3LjVHnmVrvjP0cj1PazPO16KkArQMtBLBy8S4mmLzQkuXZc=</HostId></Error>

Comment on lines +457 to +462
// `x-amz-checksum-type` and `x-amz-checksum-algorithm` are configuration
// headers (MPU completeness mode / SDK algorithm hint), not value
// headers. They must not count toward the "value header" tally.
const valueHeaders = Object.keys(headers).filter(
h => h.startsWith('x-amz-checksum-') && h !== 'x-amz-checksum-type' && h !== 'x-amz-checksum-algorithm',
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the list of supported headers is known and a short list of supported algorithms, it may be cleaner to directly extract each of the possible ones, or otherwise filter like [list-of-supported-headers].includes(h).

It would change the behavior (probably in a good way) if the client sends one or more unsupported checksums along with multiple valid ones, where we probably want to return AlgoNotSupported rather than MultipleChecksumTypes in priority in this case, but that's more a nitpick, either way should work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWS checks MultipleChecksumTypes before AlgoNotSupported, the current behavior is the same as AWS.

AWS also ignores only x-amz-checksum-type and x-amz-checksum-algorithm, they don't count them to the checksum count, and they also never trigger AlgoNotSupported. if we send x-amz-checksum-BAD on the other hand we get an AlgoNotSupported. If we send x-amz-checksum-BAD + x-amz-checksum-ZZZ we get MultipleChecksumTypes.

So the order is

  1. check no MultipleChecksumTypes
  2. check no AlgoNotSupported
  3. check actual checksum value, BadDigest if mismatch

I added a commit to ignore x-amz-checksum-type and x-amz-checksum-algorithm in the other functions, I didn't know this behavior existed.

Comment thread lib/api/completeMultipartUpload.js Outdated
partInputs.map(p => p.value),
);
} else if (type === 'FULL_OBJECT') {
result = computeFullObjectMPUChecksum(algorithm, partInputs);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering what is the worst case run time of computeFullObjectMPUChecksum, to know if it shouldn't be an async function so it can yield to the event loop regularly? Assuming the worst case is 10K parts and it's just combining CRCs, I think it should not take more than one or a few ms, so should be fine I think, but better check if not done yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test was added in the previous PR for testing the worst case, and it takes a couple of milliseconds so it should be ok

let nextCalled = false;
const callNext = (...args) => {
if (nextCalled) {
log.error('processParts: swallowed late callNext after next already invoked', {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of this style of defensive programming: for me, either we know that next may be called multiple times by design and we accept it as part of the normal behavior (e.g. with jsutil.once wrapping the inner next callback where we know it may occur), or we should raise an exception (async.* typically does). This type of error will probably be lost in the logs without anyone noticing and finish as if jsutil.once was used in the first place, potentially hiding real issues.

The original code didn't have such error handling, so I think it should be fine to assume it will not happen if the logic introduced ensures a single callback call.

@leif-scality leif-scality force-pushed the improvement/CLDSRV-898-complete-mpu-checksums-2 branch from a90bd94 to e468700 Compare May 22, 2026 13:01
@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

LGTM

The checksum handling for CompleteMultipartUpload is well-implemented: the split between md5-only body validation and final-object checksum assertion correctly matches AWS behavior. The callNext guard in processParts properly prevents double-callbacks in the mixed callback/promise flow. Per-part validation, final-object computation, and header assertion are thorough. Tests cover the full (algorithm, type) matrix including edge cases (default MPU, legacy MPU, missing parts).

Review by Claude Code

@leif-scality leif-scality force-pushed the improvement/CLDSRV-898-complete-mpu-checksums-2 branch from 46b6d78 to 6e735e4 Compare May 22, 2026 13:32
return next(typeErr, destBucket);
}
const mpuType = storedMetadata.checksumType;
if (!mpuType || headerTypeUpper !== mpuType.toUpperCase()) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When mpuType is falsy (legacy MPU created before the checksum feature, where storedMetadata.checksumType is undefined), this error message reads "The upload was created using the undefined checksum mode." — leaking an internal implementation detail to the client. Consider splitting the !mpuType and mismatch cases into separate error messages.

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

  • completeMultipartUpload.js:352 — When storedMetadata.checksumType is undefined (legacy MPU created before the checksum feature), the error message interpolates undefined into user-facing text: "The upload was created using the undefined checksum mode." Split the !mpuType guard into a separate branch with an appropriate message.

    Review by Claude Code

*/
// Validate a Content-MD5 header against the buffered body. Returns null on
// success, an error object otherwise.
function validateContentMd5(headers, body) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc block above (validateChecksumsNoChunking on lines 332-337) now documents validateContentMd5 instead, since the new function was inserted between the comment and its intended target at line 361. Move the JSDoc down to line 361 or add a proper JSDoc here for validateContentMd5.

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

LGTM — solid implementation with thorough test coverage.

One minor nit:
- Orphaned JSDoc in validateChecksums.js:332-337: the validateChecksumsNoChunking JSDoc now sits above the new validateContentMd5 function instead of its intended target.

Review by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants