UploadPart
UploadPart sends one piece of an object that is being uploaded in parts. A multipart upload
opens with CreateMultipartUpload, which hands back an upload ID;
each part then goes up with UploadPart - or is copied from an object already stored, with
UploadPartCopy - and CompleteMultipartUpload
assembles the parts into the object, given the ETag each part returned.
The part number, 1-10,000, sets the part's position in the finished object. Sending a part again under the same number replaces the earlier one, which is what makes a failed part so easy to retry: only that part travels again.
- Every part but the last is at least 5,000,000 bytes, rather than 5 MiB, so a part size chosen for Amazon S3 works unchanged. A part can be up to 5 GiB.
- A body sent with
Transfer-Encoding: chunkedand noContent-Lengthis answered411 MissingContentLength.aws-chunkeduploads, which the AWS SDKs send, work as they are. x-amz-expected-bucket-owneris welcome and has nothing to check: a key only ever reaches buckets of its own account.- A bucket in another region answers
404 NoSuchBucketrather than a redirect.
Uploaded parts stay stored, and are billed as stored data, until their upload is completed or aborted. Each upload ends well with either CompleteMultipartUpload or AbortMultipartUpload, and a lifecycle rule that aborts incomplete multipart uploads catches the ones a crashed script leaves behind - Cleaning up abandoned multipart uploads shows one.
When you'd use it
- A nightly backup of a large database dump. The backup tool sends the file in parts of, say, 64 MiB, so a dropped connection costs one part rather than the whole file, and the upload picks up where it stopped.
- A server uploading a long video recording. Several parts can go up at once, over several connections, which finishes a large upload sooner than one long request.
- A web app taking large uploads straight from the browser. The app's server starts the
upload and presigns one UploadPart URL per part; the browser sends each part with a plain
PUT, and the server completes the upload with theETags it collected. The bucket needs CORS rules for the browser to reach it. - Anything using the AWS CLI's
aws s3 cpor an SDK's transfer helper. Above a size threshold they split the file and call UploadPart for each piece, so it is often at work without being called by name.
Request
PUT /backups/backup.tar?partNumber=1&uploadId=EXAMPLE-UPLOAD-ID 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: UNSIGNED-PAYLOAD
Content-Length: 67108864
<67,108,864 bytes of part data>
Path-style, the same request is PUT /my-bucket/backups/backup.tar?partNumber=1&uploadId=EXAMPLE-UPLOAD-ID
on s3.eu-central-1.strg.com - Addressing explains both forms.
Permissions
Uploading a part takes Write objects (objects:write), the same permission that started the
upload. 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 has every permission.
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. A key with a . or .. path segment (a/../b) cannot be put in a URL, and STRG answers 400 InvalidURI. |
partNumber | Integer | Yes | The part's number, 1-10,000. Anything else is answered 400 InvalidArgument. |
uploadId | String | Yes | The upload ID CreateMultipartUpload returned, exactly as it came. |
Any other query parameter is answered with 501 NotImplemented, so a client knows straight away.
A request carrying x-amz-copy-source is UploadPartCopy rather than
UploadPart.
Request headers
| Name | Type | Required | Description |
|---|---|---|---|
Content-Length | Integer | Yes | The size of the body in bytes. A part can be up to 5 GiB. A body without it - Transfer-Encoding: chunked - is answered 411 MissingContentLength. |
Content-MD5 | String | No | The base64-encoded MD5 digest of the part, for an integrity check. A part that does not match is answered 400 BadDigest, and nothing of it is stored. |
x-amz-sdk-checksum-algorithm | String | No | The checksum algorithm the SDK used, such as CRC32, sent beside the matching x-amz-checksum-* header or trailer. An upload started with x-amz-checksum-algorithm has every part carry a checksum of that algorithm. |
x-amz-checksum-crc32 | String | No | Base64-encoded CRC32 checksum of the part. |
x-amz-checksum-crc32c | String | No | Base64-encoded CRC32C checksum of the part. |
x-amz-checksum-crc64nvme | String | No | Base64-encoded CRC64NVME checksum of the part. |
x-amz-checksum-sha1 | String | No | Base64-encoded SHA-1 digest of the part. |
x-amz-checksum-sha256 | String | No | Base64-encoded SHA-256 digest of the part. |
x-amz-checksum-sha512 | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
x-amz-checksum-md5 | String | No | Passed on with the request. Content-MD5 or one of the five checksums above covers the same need. |
x-amz-checksum-xxhash64 | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
x-amz-checksum-xxhash3 | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
x-amz-checksum-xxhash128 | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
x-amz-server-side-encryption-customer-algorithm | String | Conditional | SSE-C: AES256. Needed on every part when the upload was started with SSE-C. |
x-amz-server-side-encryption-customer-key | String | Conditional | SSE-C: the same base64-encoded 256-bit key sent to CreateMultipartUpload. |
x-amz-server-side-encryption-customer-key-MD5 | String | Conditional | SSE-C: the base64-encoded MD5 digest of the key. |
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 applicable: STRG has no Requester Pays buckets. |
Every x-amz-* header in the request is part of the signature - the AWS CLI and SDKs take care
of that. One left unsigned is answered with 403 AccessDenied.
Streaming uploads work too: an aws-chunked body (STREAMING-AWS4-HMAC-SHA256-PAYLOAD,
STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER or STREAMING-UNSIGNED-PAYLOAD-TRAILER) with
x-amz-decoded-content-length, and a trailing checksum declared in x-amz-trailer - CRC32,
CRC32C, CRC64NVME, SHA-1 or SHA-256. STRG verifies every chunk signature and the trailing
checksum, and stores nothing of a part that fails. Two combinations get a different answer:
| Request | Answer |
|---|---|
| A streaming payload in a presigned URL | 400 InvalidRequest |
A SHA-1 or SHA-256 trailer with a Content-Encoding besides aws-chunked (aws-chunked,gzip) | 501 NotImplemented. A CRC trailer works with any encoding |
The AWS SDKs' default upload behaviour works as it is; there is nothing to turn off.
Request body
The part's bytes.
Response
A successful request answers 200 OK with an empty body.
Response headers
| Name | Description |
|---|---|
ETag | The part's entity tag. Together with the part number, it is what CompleteMultipartUpload needs. |
x-amz-request-id | The request ID, handy for a support request. |
x-amz-server-side-encryption | The server-side encryption used, when the object is encrypted with STRG-managed keys (AES256). |
x-amz-server-side-encryption-customer-algorithm | For SSE-C, the algorithm that was sent. |
x-amz-server-side-encryption-customer-key-MD5 | For SSE-C, the MD5 digest of the key that was sent. |
x-amz-checksum-crc32, x-amz-checksum-crc32c, x-amz-checksum-crc64nvme, x-amz-checksum-sha1, x-amz-checksum-sha256 | The part's checksum, when the request carried one. |
x-amz-server-side-encryption-aws-kms-key-id, x-amz-server-side-encryption-bucket-key-enabled
and x-amz-request-charged never appear: STRG has no SSE-KMS and no Requester Pays.
Response body
None.
Errors
These are the answers an UploadPart can get besides 200 OK, and the situation behind each:
| Code | HTTP status | When |
|---|---|---|
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. |
InvalidArgument | 400 | partNumber is not an integer from 1 to 10,000. |
InvalidURI | 400 | The key contains a . or .. path segment, such as a/../b. Nothing is stored. |
EntityTooLarge | 400 | The declared part size is over 5 GiB. |
MissingContentLength | 411 | The body has no Content-Length. |
IncompleteBody | 400 | The body is shorter than Content-Length or x-amz-decoded-content-length declares. |
BadDigest | 400 | The part does not match Content-MD5 or an x-amz-checksum-* value, sent as a header or as a trailer. Nothing of the part is stored. |
InvalidRequest | 400 | An aws-chunked body is malformed, lacks x-amz-decoded-content-length or x-amz-trailer, carries a trailing checksum that is not a valid value, or is used in a presigned URL. |
AccessDenied | 403 | The access key lacks Write objects, or the bucket or key is outside its buckets or prefix, or an x-amz-* header is not signed, or a presigned URL has expired. |
NotImplemented | 501 | A query parameter this operation does not take, an ECDSA-signed aws-chunked body, a trailer other than the five listed above, or a SHA trailer with a combined Content-Encoding. |
InvalidAccessKeyId | 403 | The access key does not exist, has expired or been revoked, or it is not yet usable in a region where you already store data - a key created a moment ago, which works shortly afterwards. |
SignatureDoesNotMatch | 403 | The request signature, or a chunk signature of an aws-chunked body, does not match. |
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 same request can be retried. |
Differences from Amazon S3
Where STRG answers differently from Amazon S3, and what that means for a client:
| Aspect | Amazon S3 | STRG |
|---|---|---|
| Part size | 5 MiB minimum (except the last part), 5 GiB maximum | 5,000,000 bytes minimum (except the last part), 5 GiB maximum - so a part size chosen for S3 works unchanged |
| Body without a length | Transfer-Encoding: chunked accepted in some cases | 411 MissingContentLength. aws-chunked uploads work as they are |
| Streaming uploads | Every aws-chunked variant, ECDSA signatures included | The HMAC and unsigned variants, with trailing checksums CRC32, CRC32C, CRC64NVME, SHA-1 and SHA-256. A SHA trailer with a combined Content-Encoding is 501 NotImplemented; a CRC trailer works with any encoding |
| Bucket in another region | 301 redirect with x-amz-bucket-region | 404 NoSuchBucket. A bucket is reached through its own regional endpoint |
x-amz-expected-bucket-owner | Checked against the bucket owner | Nothing to check: a key reaches only its own account's buckets |
Keys with a . or .. path segment | Accepted | 400 InvalidURI |
Examples
Each example uploads part 1 of an upload started with
CreateMultipartUpload, whose upload ID is in the UPLOAD_ID
environment variable, from a file part1.bin of at least 5,000,000 bytes - unless it is the
last part - and at most 5 GiB.
Uploading a part
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api upload-part \
--bucket my-bucket \
--key backups/backup.tar \
--part-number 1 \
--upload-id "$UPLOAD_ID" \
--body ./part1.bin \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1 \
--query ETag \
--output text
import os
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
with open("./part1.bin", "rb") as part:
response = s3.upload_part(
Bucket="my-bucket",
Key="backups/backup.tar",
PartNumber=1,
UploadId=os.environ["UPLOAD_ID"],
Body=part,
)
print(response["ETag"])
import { readFile } from "node:fs/promises";
import { S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
const response = await client.send(
new UploadPartCommand({
Bucket: "my-bucket",
Key: "backups/backup.tar",
PartNumber: 1,
UploadId: process.env.UPLOAD_ID,
Body: await readFile("./part1.bin"),
}),
);
console.log(response.ETag);
For a whole file, the SDKs' transfer tools split it into parts and upload them for you - Uploading a large file on the CreateMultipartUpload page shows how.
Uploading a part through a presigned URL
A presigned URL lets a client without credentials - a browser, a partner's script - upload one
part with a plain PUT. The URL is valid for anything from 1 second to 7 days. The AWS CLI does
not presign UploadPart, so the URL comes from an SDK and the upload is shown with curl.
With its default settings, the JavaScript SDK signs the checksum of an empty body into a
presigned upload URL, so real data sent through it is answered 400 BadDigest - on Amazon S3
as on STRG. A client created with requestChecksumCalculation: "WHEN_REQUIRED", as below,
presigns URLs that work.
- Python (boto3)
- JavaScript (AWS SDK v3)
import os
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
url = s3.generate_presigned_url(
"upload_part",
Params={
"Bucket": "my-bucket",
"Key": "backups/backup.tar",
"PartNumber": 1,
"UploadId": os.environ["UPLOAD_ID"],
},
ExpiresIn=3600,
)
print(url)
Needs @aws-sdk/s3-request-presigner beside @aws-sdk/client-s3.
import { S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const presigner = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
requestChecksumCalculation: "WHEN_REQUIRED",
});
const url = await getSignedUrl(
presigner,
new UploadPartCommand({
Bucket: "my-bucket",
Key: "backups/backup.tar",
PartNumber: 1,
UploadId: process.env.UPLOAD_ID,
}),
{ expiresIn: 3600 },
);
console.log(url);
The uploader then sends the part to the URL, here held in PRESIGNED_URL, and the ETag is in
the response headers:
curl -i -X PUT --upload-file ./part1.bin "$PRESIGNED_URL"