# CompleteMultipartUpload

CompleteMultipartUpload is the last step of a multipart upload started with
[CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload). The request lists the parts - each part's
number and the `ETag` that [UploadPart](https://docs.strg.com/s3-api/UploadPart) or
[UploadPartCopy](https://docs.strg.com/s3-api/UploadPartCopy) returned for it - and STRG joins them, in part-number
order, into a new version of the object. From then on the object is there to read like any
other, and the upload ID has done its job and can no longer be used.

**Tip:**

The high-level commands of the AWS tools - `aws s3 cp`, boto3's `upload_file`, and `Upload` from
`@aws-sdk/lib-storage` - run CreateMultipartUpload, UploadPart and CompleteMultipartUpload for
you. Calling this operation directly is for code that manages the parts itself.

**Info: Good to know on STRG**

- `If-Match` and `If-None-Match` get a clear answer straight away: STRG responds
  `501 NotImplemented`, so a client knows before anything is stored.
- An empty part list is answered `400 InvalidRequest` (Amazon S3 says `400 MalformedXML`).
- Every part but the last needs at least 5,000,000 bytes (Amazon S3: 5 MiB).
- `<Location>` is the object's URL on the endpoint you called, in the addressing style you used,
  so it can be used as it is.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.

## When you'd use it

- **A nightly backup of a large archive.** A backup job splits a multi-gigabyte database dump
  into parts, uploads them - several at once, if it likes - and finishes with
  CompleteMultipartUpload. A single `PutObject` takes up to 5 GiB, so anything larger is uploaded
  this way.
- **An upload that survives a dropped connection.** A desktop app sending a long video can lose
  its network halfway through. Because each part is stored as it arrives, the app reads back what
  is already there with [ListParts](https://docs.strg.com/s3-api/ListParts), sends the rest, and completes the upload.
- **A data pipeline or an agent writing a large result.** A job producing a big export streams it
  out part by part as it is generated, and completes the upload once the last part is written.

## Request

```http
POST /photos/2024/cat.jpg?uploadId=UploadId HTTP/1.1
Host: my-bucket.s3.eu-central-1.strg.com
Authorization: AWS4-HMAC-SHA256 Credential=...
x-amz-date: 20260926T120000Z
x-amz-content-sha256: <SHA-256 of the body>
Content-Type: application/xml

<CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Part>
    <PartNumber>1</PartNumber>
    <ETag>"85f30635602dc09bd85957a6e82a2c21"</ETag>
  </Part>
  <Part>
    <PartNumber>2</PartNumber>
    <ETag>"2dc09bd8595785f3063560a6e82a2c21"</ETag>
  </Part>
</CompleteMultipartUpload>
```

Path-style, the same request is `POST /my-bucket/photos/2024/cat.jpg?uploadId=UploadId` on
`s3.eu-central-1.strg.com`. The [S3 API overview](https://docs.strg.com/s3-api/overview#addressing) describes both
addressing styles; a bucket whose name contains a dot is reached path-style.

### Permissions

The access key needs **Write objects** (`objects:write`). For a key limited to certain buckets or
to a key prefix, the bucket and the object key need to be within them.
[Access keys](https://docs.strg.com/access-keys) has the details.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket the upload was started in. |
| `Key` | String | Yes | The object key the upload was started for. |
| `uploadId` | String | Yes | The upload ID that CreateMultipartUpload returned. |

### Request headers

Every header the S3 reference lists for this operation, and what STRG does with it:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Content-MD5` | String | No | Base64 MD5 of the body. When present and it does not match, the answer is `400 BadDigest`. |
| `x-amz-checksum-crc32` | String | No | Base64 CRC32 checksum of the whole object. Accepted. |
| `x-amz-checksum-crc32c` | String | No | Base64 CRC32C checksum of the whole object. Accepted. |
| `x-amz-checksum-crc64nvme` | String | No | Base64 CRC64NVME checksum of the whole object. Accepted. |
| `x-amz-checksum-sha1` | String | No | Base64 SHA-1 digest of the whole object. Accepted. |
| `x-amz-checksum-sha256` | String | No | Base64 SHA-256 digest of the whole object. Accepted. |
| `x-amz-checksum-sha512` | String | No | Base64 SHA-512 digest of the whole object. Accepted. |
| `x-amz-checksum-md5` | String | No | Base64 MD5 digest of the whole object. Accepted. |
| `x-amz-checksum-xxhash64` | String | No | Base64 XXHASH64 checksum of the whole object. Accepted. |
| `x-amz-checksum-xxhash3` | String | No | Base64 XXHASH3 checksum of the whole object. Accepted. |
| `x-amz-checksum-xxhash128` | String | No | Base64 XXHASH128 checksum of the whole object. Accepted. |
| `x-amz-checksum-type` | String | No | `COMPOSITE` or `FULL_OBJECT`: how the part checksums combine into the object's. Accepted. |
| `x-amz-mp-object-size` | Long | No | The total object size you expect. Accepted. |
| `If-Match` | String | No | STRG answers `501 NotImplemented`. |
| `If-None-Match` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-server-side-encryption-customer-algorithm` | String | No | `AES256`, for an upload started with a customer-provided encryption key (SSE-C). Accepted. |
| `x-amz-server-side-encryption-customer-key` | String | No | The Base64 SSE-C key. Accepted. |
| `x-amz-server-side-encryption-customer-key-MD5` | String | No | Base64 MD5 of the SSE-C key. Accepted. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |
| `x-amz-request-payer` | String | No | Not needed: STRG has no Requester Pays buckets. |

The body is signed with a SHA-256 payload hash or `UNSIGNED-PAYLOAD`. For a streaming
(`STREAMING-*`) payload signature STRG answers `501 NotImplemented`; the AWS SDKs never send
this operation that way, so it concerns hand-written clients alone.

### Request body

A `CompleteMultipartUpload` XML document, up to 2 MiB. STRG checks that the body is
well-formed XML with a `CompleteMultipartUpload` root element before passing it on, and answers
`400 MalformedXML` when it is not.

| Element | Type | Required | Description |
| --- | --- | --- | --- |
| `CompleteMultipartUpload` | Container | Yes | The root element. |
| `Part` | Container | Yes, one or more | One per part to include, in ascending `PartNumber` order. Parts you uploaded but leave out are discarded. |
| `Part/PartNumber` | Integer | Yes | The part number, 1-10,000. |
| `Part/ETag` | String | Yes | The `ETag` returned when the part was uploaded, quotes included. |
| `Part/ChecksumCRC32` | String | No | The part's CRC32 checksum, as UploadPart returned it. Belongs in the list when the part was uploaded with one. |
| `Part/ChecksumCRC32C` | String | No | The part's CRC32C checksum. |
| `Part/ChecksumCRC64NVME` | String | No | The part's CRC64NVME checksum. |
| `Part/ChecksumSHA1` | String | No | The part's SHA-1 digest. |
| `Part/ChecksumSHA256` | String | No | The part's SHA-256 digest. |
| `Part/ChecksumSHA512` | String | No | The part's SHA-512 digest. |
| `Part/ChecksumMD5` | String | No | The part's MD5 digest. |
| `Part/ChecksumXXHASH64` | String | No | The part's XXHASH64 checksum. |
| `Part/ChecksumXXHASH3` | String | No | The part's XXHASH3 checksum. |
| `Part/ChecksumXXHASH128` | String | No | The part's XXHASH128 checksum. |

Part sizes: every part but the last is at least 5,000,000 bytes, and a part can be up to 5 GiB.

## Response

`200 OK` with an XML body.

**Warning: A 200 can carry an error**

As on Amazon S3, a CompleteMultipartUpload that fails after the response has started still
answers `200 OK`, with an `Error` document as its body. The AWS SDKs detect this and treat it as
an error, retrying where appropriate. A client calling the API without an SDK needs to read the
body before treating the upload as complete. [Errors](https://docs.strg.com/s3-api/CompleteMultipartUpload#errors) lists the codes such a response
can carry.

### Response headers

The headers a successful completion carries:

| Name | Description |
| --- | --- |
| `x-amz-version-id` | The version ID of the new object. Buckets are always versioned, so every completed upload creates a new version. |
| `x-amz-server-side-encryption` | `AES256` when the object is encrypted with STRG-managed keys. |
| `x-amz-server-side-encryption-aws-kms-key-id` | Never returned: STRG encrypts with its own managed keys or yours (SSE-C), and has no SSE-KMS. |
| `x-amz-server-side-encryption-bucket-key-enabled` | Never returned, for the same reason. |
| `x-amz-expiration` | The expiry date and rule ID of a lifecycle rule that applies to the object. STRG does not promise this header; the bucket's [lifecycle configuration](https://docs.strg.com/s3-api/GetBucketLifecycleConfiguration) is the dependable record of its rules. |
| `x-amz-request-id` | The request ID, handy for support requests. |

`x-amz-request-charged` is never returned.

### Response body

| Element | Description |
| --- | --- |
| `Location` | The object's URL on the endpoint you called: `https://<bucket>.s3.<region>.strg.com/<key>` virtual-hosted, `https://s3.<region>.strg.com/<bucket>/<key>` path-style. |
| `Bucket` | The bucket name. |
| `Key` | The object key. |
| `ETag` | The object's entity tag. For a multipart object it is not the MD5 of the data: it has the form `"<hex>-<number of parts>"`. |
| `ChecksumCRC32`, `ChecksumCRC32C`, `ChecksumCRC64NVME`, `ChecksumSHA1`, `ChecksumSHA256` | The object's checksum, present when the parts were uploaded with one. A composite checksum is calculated from the part checksums, not from the whole object. |
| `ChecksumSHA512`, `ChecksumMD5`, `ChecksumXXHASH64`, `ChecksumXXHASH3`, `ChecksumXXHASH128` | As above, for the corresponding algorithm. |
| `ChecksumType` | `COMPOSITE` or `FULL_OBJECT`, present with a checksum. |

```xml
<?xml version="1.0" encoding="UTF-8"?>
<CompleteMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Location>https://my-bucket.s3.eu-central-1.strg.com/photos/2024/cat.jpg</Location>
  <Bucket>my-bucket</Bucket>
  <Key>photos/2024/cat.jpg</Key>
  <ETag>"3729bf569924c845dfe90bfe6281a9ff-2"</ETag>
</CompleteMultipartUploadResult>
```

## Errors

The errors this operation can answer, and the situation behind each:

| Code | HTTP status | When |
| --- | --- | --- |
| `InvalidPart` | 400 | A listed part was not uploaded, or its `ETag` does not match the uploaded part's. |
| `InvalidPartOrder` | 400 | The parts are not in ascending `PartNumber` order. |
| `EntityTooSmall` | 400 | A part other than the last is smaller than 5,000,000 bytes. |
| `InvalidRequest` | 400 | The part list is empty. |
| `MalformedXML` | 400 | The body is not well-formed XML, or its root element is not `CompleteMultipartUpload`. |
| `MaxMessageLengthExceeded` | 400 | The body is larger than 2 MiB. |
| `BadDigest` | 400 | `Content-MD5` does not match the body. |
| `XAmzContentSHA256Mismatch` | 400 | `x-amz-content-sha256` does not match the body. |
| `NoSuchUpload` | 404 | The upload ID does not exist, or the upload was already completed or aborted. |
| `NoSuchBucket` | 404 | The bucket does not exist, it is in a region other than the endpoint's, or your account stores nothing in this endpoint's region. |
| `NotImplemented` | 501 | The request carries `If-Match`, `If-None-Match`, or a streaming payload signature. |
| `AccessDenied` | 403 | The access key lacks **Write objects**, or the bucket or key is outside its scope or prefix. |
| `InvalidAccessKeyId` | 403 | The access key does not exist, has expired or been revoked, or is not yet usable in this endpoint's region - for example a key created moments ago. |
| `SignatureDoesNotMatch` | 403 | The request signature is wrong. |
| `RequestTimeTooSkewed` | 403 | The request time is more than 15 minutes from the server's. |
| `AuthorizationHeaderMalformed` | 400 | The signature's credential scope names a region other than the endpoint's. |
| `SlowDown` | 503 | Too many requests at once. The response carries a `Retry-After` interval, and the AWS SDKs retry on their own. |
| `ServiceUnavailable` | 503 | A temporary failure. The request is safe to retry, and the AWS SDKs do so on their own. |
| `InternalError` | 500 | An internal failure. The request is safe to retry, and the AWS SDKs do so on their own. |

An error returned inside a `200 OK` (see [Response](https://docs.strg.com/s3-api/CompleteMultipartUpload#response)) carries one of `InvalidPart`,
`InvalidPartOrder`, `EntityTooSmall`, `NoSuchUpload`, `SlowDown` or `InternalError`, and no
`x-amz-version-id` or checksum headers - no object was created.

## Differences from Amazon S3

Where STRG answers differently from Amazon S3, and what that means for your code:

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Minimum part size | 5 MiB (5,242,880 bytes) for every part but the last | 5,000,000 bytes for every part but the last - so a part size chosen for S3 works unchanged |
| `If-Match`, `If-None-Match` | Conditional completion | `501 NotImplemented`, answered before anything is stored |
| Empty part list | `400 MalformedXML` | `400 InvalidRequest` |
| `Location` | The object's S3 URL | The object's URL on the STRG endpoint and addressing style you used |
| Versioning | Depends on the bucket's versioning setting | Always on: each completed upload is a new version, and every version is billed as stored data until it is deleted or a lifecycle rule removes it |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. Each bucket answers on its own region's endpoint, which the dashboard shows beside it |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Nothing to check: a key only ever reaches buckets of its own account |
| SSE-KMS | Available | STRG-managed keys (`AES256`) or your own (SSE-C) instead |

## Examples

Completing an upload for `photos/2024/cat.jpg` whose parts are already uploaded, with the part
list read back through [ListParts](https://docs.strg.com/s3-api/ListParts):

**AWS CLI**

```bash
aws s3api list-parts \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --upload-id "$UPLOAD_ID" \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1 \
  --query '{Parts: Parts[].{PartNumber: PartNumber, ETag: ETag}}' \
  --output json > parts.json

aws s3api complete-multipart-upload \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --upload-id "$UPLOAD_ID" \
  --multipart-upload file://parts.json \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import os

import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

upload_id = os.environ["UPLOAD_ID"]
parts = []
paginator = s3.get_paginator("list_parts")
for page in paginator.paginate(Bucket="my-bucket", Key="photos/2024/cat.jpg", UploadId=upload_id):
    for part in page.get("Parts", []):
        parts.append({"PartNumber": part["PartNumber"], "ETag": part["ETag"]})

result = s3.complete_multipart_upload(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    UploadId=upload_id,
    MultipartUpload={"Parts": parts},
)
print(result["ETag"], result["VersionId"])
```

**JavaScript (AWS SDK v3)**

```javascript
import {
  CompleteMultipartUploadCommand,
  S3Client,
  paginateListParts,
} from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

const input = {
  Bucket: "my-bucket",
  Key: "photos/2024/cat.jpg",
  UploadId: process.env.UPLOAD_ID,
};

const parts = [];
for await (const page of paginateListParts({ client }, input)) {
  for (const part of page.Parts ?? []) {
    parts.push({ PartNumber: part.PartNumber, ETag: part.ETag });
  }
}

const result = await client.send(
  new CompleteMultipartUploadCommand({ ...input, MultipartUpload: { Parts: parts } }),
);
console.log(result.ETag, result.VersionId);
```

A whole upload from start to finish: a local file sent in 64 MiB parts, with the upload aborted
if anything fails, so no parts are left behind:

**AWS CLI**

```bash
set -euo pipefail

ENDPOINT=https://s3.eu-central-1.strg.com
REGION=eu-central-1
BUCKET=my-bucket
KEY=backups/archive.tar

UPLOAD_ID=$(aws s3api create-multipart-upload \
  --bucket "$BUCKET" --key "$KEY" \
  --endpoint-url "$ENDPOINT" --region "$REGION" \
  --query UploadId --output text)
trap 'aws s3api abort-multipart-upload --bucket "$BUCKET" --key "$KEY" --upload-id "$UPLOAD_ID" --endpoint-url "$ENDPOINT" --region "$REGION"' ERR

WORKDIR=$(mktemp -d)
split -b 64M -d -a 5 archive.tar "$WORKDIR/part-"

number=1
for file in "$WORKDIR"/part-*; do
  aws s3api upload-part \
    --bucket "$BUCKET" --key "$KEY" --upload-id "$UPLOAD_ID" \
    --part-number "$number" --body "$file" \
    --endpoint-url "$ENDPOINT" --region "$REGION" > /dev/null
  number=$((number + 1))
done

aws s3api list-parts \
  --bucket "$BUCKET" --key "$KEY" --upload-id "$UPLOAD_ID" \
  --endpoint-url "$ENDPOINT" --region "$REGION" \
  --query '{Parts: Parts[].{PartNumber: PartNumber, ETag: ETag}}' \
  --output json > "$WORKDIR/parts.json"

aws s3api complete-multipart-upload \
  --bucket "$BUCKET" --key "$KEY" --upload-id "$UPLOAD_ID" \
  --multipart-upload "file://$WORKDIR/parts.json" \
  --endpoint-url "$ENDPOINT" --region "$REGION"

rm -r "$WORKDIR"
```

**Python (boto3)**

```python
import boto3

PART_SIZE = 64 * 1024 * 1024

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

bucket, key = "my-bucket", "backups/archive.tar"
upload_id = s3.create_multipart_upload(Bucket=bucket, Key=key)["UploadId"]

try:
    parts = []
    with open("archive.tar", "rb") as file:
        number = 1
        while chunk := file.read(PART_SIZE):
            response = s3.upload_part(
                Bucket=bucket, Key=key, UploadId=upload_id, PartNumber=number, Body=chunk
            )
            parts.append({"PartNumber": number, "ETag": response["ETag"]})
            number += 1

    s3.complete_multipart_upload(
        Bucket=bucket, Key=key, UploadId=upload_id, MultipartUpload={"Parts": parts}
    )
except Exception:
    s3.abort_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id)
    raise
```

**JavaScript (AWS SDK v3)**

```javascript
import { open } from "node:fs/promises";

import {
  AbortMultipartUploadCommand,
  CompleteMultipartUploadCommand,
  CreateMultipartUploadCommand,
  S3Client,
  UploadPartCommand,
} from "@aws-sdk/client-s3";

const PART_SIZE = 64 * 1024 * 1024;

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

const Bucket = "my-bucket";
const Key = "backups/archive.tar";
const { UploadId } = await client.send(new CreateMultipartUploadCommand({ Bucket, Key }));

try {
  const parts = [];
  const file = await open("archive.tar");
  try {
    for (let PartNumber = 1; ; PartNumber++) {
      const buffer = Buffer.alloc(PART_SIZE);
      const { bytesRead } = await file.read(buffer, 0, PART_SIZE, null);
      if (bytesRead === 0) break;
      const { ETag } = await client.send(
        new UploadPartCommand({ Bucket, Key, UploadId, PartNumber, Body: buffer.subarray(0, bytesRead) }),
      );
      parts.push({ PartNumber, ETag });
    }
  } finally {
    await file.close();
  }

  await client.send(
    new CompleteMultipartUploadCommand({ Bucket, Key, UploadId, MultipartUpload: { Parts: parts } }),
  );
} catch (error) {
  await client.send(new AbortMultipartUploadCommand({ Bucket, Key, UploadId }));
  throw error;
}
```

**Danger: Unfinished uploads are billed**

The parts of an upload that is never completed or aborted are billed as stored data. Every upload
that is started wants to end in CompleteMultipartUpload or
[AbortMultipartUpload](https://docs.strg.com/s3-api/AbortMultipartUpload); a lifecycle rule that aborts incomplete
multipart uploads automatically is a good safety net - [Lifecycle
policies](../lifecycle-policies.mdx#cleaning-up-abandoned-multipart-uploads) shows one.

## Related

- [CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload)
- [UploadPart](https://docs.strg.com/s3-api/UploadPart)
- [UploadPartCopy](https://docs.strg.com/s3-api/UploadPartCopy)
- [AbortMultipartUpload](https://docs.strg.com/s3-api/AbortMultipartUpload)
- [ListParts](https://docs.strg.com/s3-api/ListParts)
- [ListMultipartUploads](https://docs.strg.com/s3-api/ListMultipartUploads)
- [PutObject](https://docs.strg.com/s3-api/PutObject)
- [Lifecycle policies](https://docs.strg.com/lifecycle-policies)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
