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.
- 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-MatchandIf-Match(conditional writes),x-amz-tagging,x-amz-write-offset-bytes, event holds, SSE-KMS andx-amz-grant-*get a501 NotImplementedanswer, so a client knows straight away.- An object is always exactly as public as its bucket:
x-amz-acltakesprivateorpublic-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 anaws-chunkedupload; one with neither is answered411 MissingContentLength. - A key with a
.or..path segment (a/../b,a/./b, or their%2eforms) is answered400 InvalidURI, and nothing is stored. x-amz-expected-bucket-owneris 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.gzwith 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.jsonat 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:
| Permission | Dashboard label | For |
|---|---|---|
objects:write | Write objects | Every upload |
object-retention:write | Set retention | Also needed when the request carries x-amz-object-lock-mode or x-amz-object-lock-retain-until-date |
object-legal-hold:write | Set legal holds | Also 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
| Name | Type | Required | Description |
|---|---|---|---|
Bucket | String | Yes | The bucket to upload to, in the endpoint's region. |
Key | String | Yes | The 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.
| Name | Type | Required | Description |
|---|---|---|---|
Content-Length | Integer | Yes | The 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-Type | String | No | Stored with the object and returned when it is read. |
Content-Encoding | String | No | Stored 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-Disposition | String | No | Stored with the object and returned when it is read. |
Content-Language | String | No | Stored with the object and returned when it is read. |
Cache-Control | String | No | Stored with the object and returned when it is read. |
Expires | Timestamp | No | Stored with the object and returned when it is read. |
Content-MD5 | String | No | The base64 MD5 digest of the body, for an integrity check. |
x-amz-meta-* | String | No | User metadata, one header per entry. Returned by GetObject and HeadObject. |
x-amz-content-sha256 | String | Yes, unless presigned | The 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-length | Integer | For aws-chunked | The object's size in bytes. An aws-chunked upload without it is answered 400 InvalidRequest. |
x-amz-trailer | String | For a trailer | The 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-algorithm | String | No | Sent by the SDKs alongside a checksum, and welcome. |
x-amz-checksum-crc32 | String | No | The base64 CRC32 of the object. Verified and stored; a mismatch is answered 400 BadDigest. |
x-amz-checksum-crc32c | String | No | The base64 CRC32C of the object. Verified and stored. |
x-amz-checksum-crc64nvme | String | No | The base64 CRC64NVME of the object. Verified and stored. |
x-amz-checksum-sha1 | String | No | The base64 SHA-1 of the object. Verified and stored. |
x-amz-checksum-sha256 | String | No | The base64 SHA-256 of the object. Verified and stored. |
x-amz-checksum-sha512 | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
x-amz-checksum-md5 | String | No | Not one of the algorithms STRG verifies and stores; Content-MD5 or one of the five above covers the same need. |
x-amz-checksum-xxhash64 | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
x-amz-checksum-xxhash3 | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
x-amz-checksum-xxhash128 | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
If-Match | String | No | STRG answers 501 NotImplemented. |
If-None-Match | String | No | STRG answers 501 NotImplemented, so there is no create-only-if-absent upload. |
x-amz-acl | String | No | private 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-control | String | No | STRG answers 501 NotImplemented. |
x-amz-grant-read | String | No | STRG answers 501 NotImplemented. |
x-amz-grant-read-acp | String | No | STRG answers 501 NotImplemented. |
x-amz-grant-write-acp | String | No | STRG answers 501 NotImplemented. |
x-amz-tagging | String | No | STRG answers 501 NotImplemented: objects carry no tags. |
x-amz-write-offset-bytes | Integer | No | STRG answers 501 NotImplemented: an object is written whole, never appended to. |
x-amz-server-side-encryption | String | No | AES256 - 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-id | String | No | STRG answers 501 NotImplemented. |
x-amz-server-side-encryption-context | String | No | STRG answers 501 NotImplemented. |
x-amz-server-side-encryption-bucket-key-enabled | Boolean | No | STRG answers 501 NotImplemented. |
x-amz-server-side-encryption-customer-algorithm | String | No | AES256, to encrypt the object with your own key (SSE-C). Reading it back takes the same key. |
x-amz-server-side-encryption-customer-key | String | No | The base64 256-bit key to encrypt the object with (SSE-C). It is not stored. |
x-amz-server-side-encryption-customer-key-MD5 | String | No | The base64 MD5 digest of that key. |
x-amz-object-lock-mode | String | No | GOVERNANCE 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-date | Timestamp | No | When the object's retention ends, in ISO 8601 format. Needs Set retention. |
x-amz-object-lock-legal-hold | String | No | ON or OFF, for a bucket with Object Lock enabled. Needs Set legal holds. |
x-amz-object-lock-event-hold | String | No | STRG answers 501 NotImplemented. |
x-amz-object-lock-event-hold-duration-days | Integer | No | STRG answers 501 NotImplemented. |
x-amz-object-lock-event-hold-duration-years | Integer | No | STRG answers 501 NotImplemented. |
x-amz-storage-class | String | No | STRG has one storage class, STANDARD, so the header can be left out or sent as STANDARD. |
x-amz-website-redirect-location | String | No | STRG does not host bucket websites, so this header has no effect to count on. |
x-amz-request-payer | String | No | Has nothing to do: STRG has no Requester Pays buckets. |
x-amz-expected-bucket-owner | String | No | Welcome, and has nothing to check: an access key only ever reaches its own account's buckets. |
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
| Name | Description |
|---|---|
ETag | The entity tag of the new object. |
x-amz-version-id | The 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-sha256 | The checksum stored with the object, when the upload carried one. |
x-amz-server-side-encryption | AES256 when the object is encrypted with STRG-managed keys. |
x-amz-server-side-encryption-customer-algorithm | AES256 when the object is encrypted with your own key. |
x-amz-server-side-encryption-customer-key-MD5 | The MD5 digest of your key, for SSE-C. |
x-amz-request-id | The 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.
| Code | HTTP status | When |
|---|---|---|
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. |
BadDigest | 400 | The body does not match Content-MD5 or an x-amz-checksum-* value, sent as a header or as a trailer. Nothing is stored. |
EntityTooLarge | 400 | The declared object size - Content-Length, or x-amz-decoded-content-length for an aws-chunked upload - is over 5 GiB. Nothing is stored. |
IncompleteBody | 400 | The body is shorter than Content-Length or x-amz-decoded-content-length declared. |
InvalidRequest | 400 | An aws-chunked upload is malformed or lacks x-amz-decoded-content-length or x-amz-trailer, or a presigned URL declares a streaming payload. |
InvalidURI | 400 | The key contains a . or .. path segment, such as a/../b. Nothing is stored. |
InvalidArgument | 400 | x-amz-content-sha256 is not a SHA-256 hex digest or one of the accepted values. |
MissingContentLength | 411 | The body has no Content-Length and is not an aws-chunked upload. |
NotImplemented | 501 | The request carries a header or query parameter STRG does not offer - Request headers lists each one. |
AccessDenied | 403 | The 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. |
InvalidAccessKeyId | 403 | The 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. |
SignatureDoesNotMatch | 403 | The request signature, a chunk signature or the trailer signature does not match. Nothing is stored. |
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 AWS CLI and SDKs retry after the Retry-After interval. |
ServiceUnavailable | 503 | A 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.
| Aspect | Amazon S3 | STRG |
|---|---|---|
| Bucket in another region | 301 redirect with x-amz-bucket-region | 404 NoSuchBucket. The bucket's own regional endpoint reaches it |
Conditional writes (If-None-Match: *, If-Match) | Supported | 501 NotImplemented |
| Versioning | Depends on the bucket | Always on: an upload to an existing key adds a version and keeps the previous one |
| ACLs | Any canned ACL and x-amz-grant-* | private or public-read, matching the bucket; grants are answered 501 |
| Tags | x-amz-tagging | 501 NotImplemented. Objects carry no tags |
| SSE-KMS | Supported | 501 NotImplemented. AES256 and SSE-C both encrypt the object |
| Checksum algorithms | CRC32, CRC32C, CRC64NVME, SHA-1, SHA-256, SHA-512, MD5, XXHASH64, XXHASH3, XXHASH128 | CRC32, CRC32C, CRC64NVME, SHA-1 and SHA-256 |
SHA-1 or SHA-256 trailer with a compressed Content-Encoding | Supported | 501 NotImplemented. A CRC checksum suits a compressed upload |
| Object Lock headers | s3:PutObject plus the Object Lock actions | Write objects plus Set retention or Set legal holds |
| Event holds | Supported | 501 NotImplemented |
Appending (x-amz-write-offset-bytes) | Directory buckets only | 501 NotImplemented |
| Storage classes | Several | One, STANDARD |
Transfer-Encoding: chunked without Content-Length | Accepted in some cases | 411 MissingContentLength. aws-chunked uploads work as usual |
x-amz-expected-bucket-owner | Checked against the bucket owner | Welcome, with nothing to check |
Keys with a . or .. path segment (a/../b, a/./b) | Stored under the key as sent | 400 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 CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
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
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
with open("cat.jpg", "rb") as body:
response = s3.put_object(
Bucket="my-bucket",
Key="photos/2024/cat.jpg",
Body=body,
ContentType="image/jpeg",
Metadata={"photographer": "alex"},
)
print(response["ETag"], response["VersionId"])
import { readFile } from "node:fs/promises";
import { PutObjectCommand, S3Client } 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 PutObjectCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
Body: await readFile("cat.jpg"),
ContentType: "image/jpeg",
Metadata: { photographer: "alex" },
}),
);
console.log(response.ETag, response.VersionId);
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 CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
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
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
with open("cat.jpg", "rb") as body:
response = s3.put_object(
Bucket="my-bucket",
Key="photos/2024/cat.jpg",
Body=body,
ChecksumAlgorithm="SHA256",
)
print(response["ChecksumSHA256"])
import { readFile } from "node:fs/promises";
import { PutObjectCommand, S3Client } 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 PutObjectCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
Body: await readFile("cat.jpg"),
ChecksumAlgorithm: "SHA256",
}),
);
console.log(response.ChecksumSHA256);
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 CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
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
from datetime import datetime, timezone
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
with open("cat.jpg", "rb") as body:
s3.put_object(
Bucket="my-bucket",
Key="photos/2024/cat.jpg",
Body=body,
ObjectLockMode="GOVERNANCE",
ObjectLockRetainUntilDate=datetime(2027, 1, 1, tzinfo=timezone.utc),
)
import { readFile } from "node:fs/promises";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
await client.send(
new PutObjectCommand({
Bucket: "my-bucket",
Key: "photos/2024/cat.jpg",
Body: await readFile("cat.jpg"),
ObjectLockMode: "GOVERNANCE",
ObjectLockRetainUntilDate: new Date("2027-01-01T00:00:00Z"),
}),
);
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 CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
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
import boto3
from boto3.s3.transfer import TransferConfig
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
s3.upload_file(
"backup.tar",
"my-bucket",
"backups/backup.tar",
Config=TransferConfig(multipart_chunksize=64 * 1024 * 1024),
)
Needs @aws-sdk/lib-storage beside @aws-sdk/client-s3.
import { createReadStream } from "node:fs";
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
await new Upload({
client,
params: {
Bucket: "my-bucket",
Key: "backups/backup.tar",
Body: createReadStream("backup.tar"),
},
partSize: 64 * 1024 * 1024,
}).done();
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.
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.
- Python (boto3)
- JavaScript (AWS SDK v3)
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"])
Needs @aws-sdk/s3-request-presigner beside @aws-sdk/client-s3.
import { readFile } from "node:fs/promises";
import { PutObjectCommand, S3Client } 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 PutObjectCommand({ Bucket: "my-bucket", Key: "photos/2024/cat.jpg" }),
{ expiresIn: 3600 },
);
// What the uploader runs:
const response = await fetch(url, { method: "PUT", body: await readFile("cat.jpg") });
console.log(response.status, response.headers.get("etag"));
The AWS CLI's aws s3 presign makes download URLs, so an upload URL comes from an SDK.