Skip to main content

PutObject

PutObject stores an object in a bucket in one request: the bytes in the body, and the object's content type, metadata, checksum, encryption and Object Lock settings in the headers. Every STRG bucket keeps versions, so an upload to a key that already exists adds a new version and the previous one stays safely in place. Every version is billed as stored data until it is deleted or a lifecycle policy removes it.

A single PutObject stores up to 5 GiB. A larger object travels as a multipart upload - CreateMultipartUpload, UploadPart and CompleteMultipartUpload - which the high-level upload commands take care of on their own, as Uploading a large file shows.

Good to know on STRG
  • Buckets are always versioned: an upload to an existing key adds a version and keeps the one before it.
  • Checksums are verified and stored for CRC32, CRC32C, CRC64NVME, SHA-1 and SHA-256.
  • If-None-Match and If-Match (conditional writes), x-amz-tagging, x-amz-write-offset-bytes, event holds, SSE-KMS and x-amz-grant-* get a 501 NotImplemented answer, so a client knows straight away.
  • An object is always exactly as public as its bucket: x-amz-acl takes private or public-read, the value matching the bucket's Bucket Protection setting.
  • Setting Object Lock on upload takes Set retention or Set legal holds on the access key, beside Write objects.
  • A body travels with a Content-Length, or as an aws-chunked upload; one with neither is answered 411 MissingContentLength.
  • A key with a . or .. path segment (a/../b, a/./b, or their %2e forms) is answered 400 InvalidURI, and nothing is stored.
  • 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 web app taking uploads. A photo-sharing app stores each profile picture its users send. Its server can upload them itself, or hand the browser a presigned PUT URL so the file goes straight to STRG - with CORS rules on the bucket letting the browser through.
  • A nightly backup. A backup script writes the day's database dump as backups/2026-09-26.sql.gz with one PutObject; a dump that grows past 5 GiB travels as a multipart upload instead.
  • An agent keeping its memory. An AI agent writes its notes to memory/notes.json at the end of every session. Each upload is a new version, so yesterday's notes are still there if today's turn out wrong.
  • Records that must not change. An accounting system stores each invoice PDF with a retention date in a bucket with Object Lock, so it cannot be altered or deleted before its retention period ends.

Request​

PUT /photos/2024/cat.jpg 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-Type: image/jpeg
Content-Length: 48213
x-amz-meta-photographer: alex

<object data>

Path-style, the same request is PUT /my-bucket/photos/2024/cat.jpg on s3.eu-central-1.strg.com, which is how a bucket whose name contains a dot is reached - Addressing has both styles.

Every x-amz-* header is part of the signature; the AWS CLI and SDKs take care of that.

Permissions​

An upload takes Write objects, and setting Object Lock on the new version takes one more permission per header:

PermissionDashboard labelFor
objects:writeWrite objectsEvery upload
object-retention:writeSet retentionAlso needed when the request carries x-amz-object-lock-mode or x-amz-object-lock-retain-until-date
object-legal-hold:writeSet legal holdsAlso needed when the request carries x-amz-object-lock-legal-hold

For a key that reaches certain buckets or an object prefix, the bucket and the object key fall within them; an upload reaching outside them is answered 403 AccessDenied. Access keys has the details.

URI parameters​

NameTypeRequiredDescription
BucketStringYesThe bucket to upload to, in the endpoint's region.
KeyStringYesThe object key. A key with a . or .. path segment (a/../b, a/./b, ./a, percent-encoded as %2e or not) is answered 400 InvalidURI, and nothing is stored under either name.

PutObject takes no query parameters. A query parameter S3 does not define for PutObject gets a 501 NotImplemented answer. Presigned URLs carry their X-Amz-* signing parameters in the query as usual.

Request headers​

The headers below are every one S3 defines for PutObject, each with what STRG does with it.

NameTypeRequiredDescription
Content-LengthIntegerYesThe size of the body in bytes. A plain body without it is answered 411 MissingContentLength. For an aws-chunked body it is the framed length, and x-amz-decoded-content-length carries the object's size.
Content-TypeStringNoStored with the object and returned when it is read.
Content-EncodingStringNoStored with the object and returned when it is read - gzip, for example. The body is stored exactly as sent. For an aws-chunked upload, aws-chunked is taken out and the rest is stored.
Content-DispositionStringNoStored with the object and returned when it is read.
Content-LanguageStringNoStored with the object and returned when it is read.
Cache-ControlStringNoStored with the object and returned when it is read.
ExpiresTimestampNoStored with the object and returned when it is read.
Content-MD5StringNoThe base64 MD5 digest of the body, for an integrity check.
x-amz-meta-*StringNoUser metadata, one header per entry. Returned by GetObject and HeadObject.
x-amz-content-sha256StringYes, unless presignedThe SHA-256 of the body in hex, UNSIGNED-PAYLOAD, or one of the aws-chunked streaming values - Authentication lists them. A streaming value in a presigned URL is answered 400 InvalidRequest. The SDKs set it.
x-amz-decoded-content-lengthIntegerFor aws-chunkedThe object's size in bytes. An aws-chunked upload without it is answered 400 InvalidRequest.
x-amz-trailerStringFor a trailerThe checksum sent after the body in an aws-chunked upload: x-amz-checksum-crc32, -crc32c, -crc64nvme, -sha1 or -sha256. Any other is answered 501 NotImplemented. A SHA-1 or SHA-256 trailer together with a Content-Encoding other than aws-chunked is answered 501 NotImplemented too; a CRC checksum suits a compressed upload.
x-amz-sdk-checksum-algorithmStringNoSent by the SDKs alongside a checksum, and welcome.
x-amz-checksum-crc32StringNoThe base64 CRC32 of the object. Verified and stored; a mismatch is answered 400 BadDigest.
x-amz-checksum-crc32cStringNoThe base64 CRC32C of the object. Verified and stored.
x-amz-checksum-crc64nvmeStringNoThe base64 CRC64NVME of the object. Verified and stored.
x-amz-checksum-sha1StringNoThe base64 SHA-1 of the object. Verified and stored.
x-amz-checksum-sha256StringNoThe base64 SHA-256 of the object. Verified and stored.
x-amz-checksum-sha512StringNoNot one of the algorithms STRG verifies and stores - the five above are.
x-amz-checksum-md5StringNoNot one of the algorithms STRG verifies and stores; Content-MD5 or one of the five above covers the same need.
x-amz-checksum-xxhash64StringNoNot one of the algorithms STRG verifies and stores - the five above are.
x-amz-checksum-xxhash3StringNoNot one of the algorithms STRG verifies and stores - the five above are.
x-amz-checksum-xxhash128StringNoNot one of the algorithms STRG verifies and stores - the five above are.
If-MatchStringNoSTRG answers 501 NotImplemented.
If-None-MatchStringNoSTRG answers 501 NotImplemented, so there is no create-only-if-absent upload.
x-amz-aclStringNoprivate or public-read, matching the bucket: private for a bucket with Bucket Protection on, public-read for one with it off. Any other canned ACL is answered 501 NotImplemented. An object is always exactly as public as its bucket.
x-amz-grant-full-controlStringNoSTRG answers 501 NotImplemented.
x-amz-grant-readStringNoSTRG answers 501 NotImplemented.
x-amz-grant-read-acpStringNoSTRG answers 501 NotImplemented.
x-amz-grant-write-acpStringNoSTRG answers 501 NotImplemented.
x-amz-taggingStringNoSTRG answers 501 NotImplemented: objects carry no tags.
x-amz-write-offset-bytesIntegerNoSTRG answers 501 NotImplemented: an object is written whole, never appended to.
x-amz-server-side-encryptionStringNoAES256 - server-side encryption with STRG-managed keys. aws:kms and aws:kms:dsse are answered 501 NotImplemented. Without it, the bucket's default encryption applies - PutBucketEncryption sets it.
x-amz-server-side-encryption-aws-kms-key-idStringNoSTRG answers 501 NotImplemented.
x-amz-server-side-encryption-contextStringNoSTRG answers 501 NotImplemented.
x-amz-server-side-encryption-bucket-key-enabledBooleanNoSTRG answers 501 NotImplemented.
x-amz-server-side-encryption-customer-algorithmStringNoAES256, to encrypt the object with your own key (SSE-C). Reading it back takes the same key.
x-amz-server-side-encryption-customer-keyStringNoThe base64 256-bit key to encrypt the object with (SSE-C). It is not stored.
x-amz-server-side-encryption-customer-key-MD5StringNoThe base64 MD5 digest of that key.
x-amz-object-lock-modeStringNoGOVERNANCE or COMPLIANCE, with x-amz-object-lock-retain-until-date, for a bucket with Object Lock enabled. Needs Set retention. Object Lock explains the modes.
x-amz-object-lock-retain-until-dateTimestampNoWhen the object's retention ends, in ISO 8601 format. Needs Set retention.
x-amz-object-lock-legal-holdStringNoON or OFF, for a bucket with Object Lock enabled. Needs Set legal holds.
x-amz-object-lock-event-holdStringNoSTRG answers 501 NotImplemented.
x-amz-object-lock-event-hold-duration-daysIntegerNoSTRG answers 501 NotImplemented.
x-amz-object-lock-event-hold-duration-yearsIntegerNoSTRG answers 501 NotImplemented.
x-amz-storage-classStringNoSTRG has one storage class, STANDARD, so the header can be left out or sent as STANDARD.
x-amz-website-redirect-locationStringNoSTRG does not host bucket websites, so this header has no effect to count on.
x-amz-request-payerStringNoHas nothing to do: STRG has no Requester Pays buckets.
x-amz-expected-bucket-ownerStringNoWelcome, and has nothing to check: an access key only ever reaches its own account's buckets.
A locked object cannot be deleted

In a bucket with Object Lock enabled, an object uploaded with a retention or a legal hold - or under the bucket's default retention - cannot be deleted until the lock ends, and stays billed as stored data until then. A retention in COMPLIANCE mode cannot be shortened or removed by anyone. Object Lock explains both modes, and is worth reading before setting one.

Request body​

The object's bytes, stored exactly as sent, up to 5 GiB; Uploading a large file covers anything larger.

A body sent with Transfer-Encoding: chunked and no Content-Length is answered 411 MissingContentLength. aws-chunked uploads - what the AWS SDKs send by default for a streamed body - work too: each chunk signature and the trailing checksum are verified, and a failure stores nothing.

Response​

A successful upload answers 200 OK with no body.

Response headers​

NameDescription
ETagThe entity tag of the new object.
x-amz-version-idThe version ID of the new object. Buckets are always versioned, so every upload creates a new version.
x-amz-checksum-crc32, x-amz-checksum-crc32c, x-amz-checksum-crc64nvme, x-amz-checksum-sha1, x-amz-checksum-sha256The checksum stored with the object, when the upload carried one.
x-amz-server-side-encryptionAES256 when the object is encrypted with STRG-managed keys.
x-amz-server-side-encryption-customer-algorithmAES256 when the object is encrypted with your own key.
x-amz-server-side-encryption-customer-key-MD5The MD5 digest of your key, for SSE-C.
x-amz-request-idThe request ID, handy when contacting support.

Version IDs are opaque strings, best stored and sent back exactly as they are.

x-amz-expiration, x-amz-object-size, x-amz-request-charged, x-amz-server-side-encryption-aws-kms-key-id, x-amz-server-side-encryption-context, x-amz-server-side-encryption-bucket-key-enabled and the x-amz-checksum-* headers of the other algorithms are not part of the answer, and x-amz-checksum-type is not something to count on.

Response body​

None.

Errors​

These are the answers an upload can get instead of 200 OK, and the situation behind each.

CodeHTTP statusWhen
NoSuchBucket404The bucket does not exist, it is in a region other than the endpoint's, or your account stores nothing in this endpoint's region.
BadDigest400The body does not match Content-MD5 or an x-amz-checksum-* value, sent as a header or as a trailer. Nothing is stored.
EntityTooLarge400The declared object size - Content-Length, or x-amz-decoded-content-length for an aws-chunked upload - is over 5 GiB. Nothing is stored.
IncompleteBody400The body is shorter than Content-Length or x-amz-decoded-content-length declared.
InvalidRequest400An aws-chunked upload is malformed or lacks x-amz-decoded-content-length or x-amz-trailer, or a presigned URL declares a streaming payload.
InvalidURI400The key contains a . or .. path segment, such as a/../b. Nothing is stored.
InvalidArgument400x-amz-content-sha256 is not a SHA-256 hex digest or one of the accepted values.
MissingContentLength411The body has no Content-Length and is not an aws-chunked upload.
NotImplemented501The request carries a header or query parameter STRG does not offer - Request headers lists each one.
AccessDenied403The access key lacks Write objects, or the Object Lock permission a lock header needs, or the bucket or key is outside its buckets or object prefix. Also an expired presigned URL, and an unsigned x-amz-* header.
InvalidAccessKeyId403The access key does not exist, has expired or been revoked, or was created moments ago and is not yet usable in this region, where you already store data. It becomes usable shortly after.
SignatureDoesNotMatch403The request signature, a chunk signature or the trailer signature does not match. Nothing is stored.
RequestTimeTooSkewed403The request time is more than 15 minutes from the server's.
AuthorizationHeaderMalformed400The signature's credential scope names a region other than the endpoint's.
SlowDown503Too many requests at once. The AWS CLI and SDKs retry after the Retry-After interval.
ServiceUnavailable503A temporary failure, which a retry of the request gets past.

Differences from Amazon S3​

Each row is something STRG does its own way, and what that means for an upload.

AspectAmazon S3STRG
Bucket in another region301 redirect with x-amz-bucket-region404 NoSuchBucket. The bucket's own regional endpoint reaches it
Conditional writes (If-None-Match: *, If-Match)Supported501 NotImplemented
VersioningDepends on the bucketAlways on: an upload to an existing key adds a version and keeps the previous one
ACLsAny canned ACL and x-amz-grant-*private or public-read, matching the bucket; grants are answered 501
Tagsx-amz-tagging501 NotImplemented. Objects carry no tags
SSE-KMSSupported501 NotImplemented. AES256 and SSE-C both encrypt the object
Checksum algorithmsCRC32, CRC32C, CRC64NVME, SHA-1, SHA-256, SHA-512, MD5, XXHASH64, XXHASH3, XXHASH128CRC32, CRC32C, CRC64NVME, SHA-1 and SHA-256
SHA-1 or SHA-256 trailer with a compressed Content-EncodingSupported501 NotImplemented. A CRC checksum suits a compressed upload
Object Lock headerss3:PutObject plus the Object Lock actionsWrite objects plus Set retention or Set legal holds
Event holdsSupported501 NotImplemented
Appending (x-amz-write-offset-bytes)Directory buckets only501 NotImplemented
Storage classesSeveralOne, STANDARD
Transfer-Encoding: chunked without Content-LengthAccepted in some cases411 MissingContentLength. aws-chunked uploads work as usual
x-amz-expected-bucket-ownerChecked against the bucket ownerWelcome, with nothing to check
Keys with a . or .. path segment (a/../b, a/./b)Stored under the key as sent400 InvalidURI; nothing is stored. A key without such segments works everywhere

Examples​

Uploading a file​

Uploading cat.jpg as photos/2024/cat.jpg, with its content type and one metadata entry:

aws s3api put-object \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--body cat.jpg \
--content-type image/jpeg \
--metadata photographer=alex \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1

Uploading with a SHA-256 checksum​

The client computes the checksum, and STRG verifies it, stores it with the object and returns it from GetObject and HeadObject when checksum mode is enabled - a simple way to know the bytes arrived exactly as they left:

aws s3api put-object \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--body cat.jpg \
--checksum-algorithm SHA256 \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1

Uploading with a retention​

In a bucket with Object Lock enabled, this locks the new version in governance mode until the start of 2027. The access key holds Set retention as well as Write objects:

aws s3api put-object \
--bucket my-bucket \
--key photos/2024/cat.jpg \
--body cat.jpg \
--object-lock-mode GOVERNANCE \
--object-lock-retain-until-date 2027-01-01T00:00:00Z \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1

Uploading a large file​

A single PutObject stores up to 5 GiB, and a multipart upload is the better fit well before that size: parts go up in parallel, and a part that fails is retried on its own rather than restarting the whole file. The high-level upload commands switch to a multipart upload automatically. The examples set a 64 MiB part size; any size from 5 MB (5,000,000 bytes) to 5 GiB works, with up to 10,000 parts per upload:

aws configure set default.s3.multipart_chunksize 64MB

aws s3 cp backup.tar s3://my-bucket/backups/backup.tar \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1

Taking uploads through a presigned URL​

A presigned PUT URL lets someone without an access key upload one object - a browser, a partner's script, a phone app. The URL is valid for anything from 1 second to 7 days, and the uploader sends a plain PUT with the body.

Presigning with the JavaScript SDK

With its default settings, the JavaScript SDK signs the checksum of an empty body into a presigned PUT URL, so every upload of real data through it fails with 400 BadDigest - on Amazon S3 as on STRG. A client created with requestChecksumCalculation: "WHEN_REQUIRED", as below, presigns URLs that work.

import boto3
import urllib.request

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

url = s3.generate_presigned_url(
"put_object",
Params={"Bucket": "my-bucket", "Key": "photos/2024/cat.jpg"},
ExpiresIn=3600,
)

# What the uploader runs:
with open("cat.jpg", "rb") as body:
request = urllib.request.Request(
url, data=body.read(), method="PUT", headers={"Content-Type": "image/jpeg"}
)
with urllib.request.urlopen(request) as response:
print(response.status, response.headers["ETag"])

The AWS CLI's aws s3 presign makes download URLs, so an upload URL comes from an SDK.

View as Markdown