PutBucketCors
PutBucketCors sets a bucket's CORS configuration, replacing every rule it had with the set in the request. Changing one rule therefore means reading the current set with GetBucketCors, editing it and sending the whole set back; removing every rule is the work of DeleteBucketCors. The dashboard and this operation edit the same rules. CORS rules explains the rule format, the limits and how rules are stored.
- Changing CORS rules is the work of an access key for all buckets holding Manage buckets.
- STRG checks each rule as it is saved, so a typo shows up right away rather than as a
puzzling browser error later. A bucket holds up to 100 rules,
MaxAgeSecondsgoes up to 86,400, and each rule has a budget of 1,000 bytes. - Rules are stored in a tidy, consistent form:
IDis set aside, origins and header names are lowercased, and methods may come back in a different order. - On a public bucket, anonymous readers can still get answers carrying the old CORS headers for up to 5 minutes after the change.
x-amz-expected-bucket-owneris welcome and has nothing to check.
When you'd use it
- A web app lets people upload a profile photo straight from the browser, with a presigned URL. The browser only sends that upload if the bucket's CORS rules allow the app's origin, so the app's deploy script sets them with PutBucketCors.
- A team adds a staging site next to production. Its deploy script reads the current rules, adds the staging origin and writes the full set back, so both sites keep working.
- A team keeps its bucket configuration in version control, and a release job writes the rules from the repository to the bucket, so what the repository says is what the bucket does.
- An agent building a small static site for someone sets the rules its pages need to fetch images and data from the bucket.
Request
PUT /?cors 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, hex>
Content-MD5: <MD5 of the body, base64>
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>https://www.example.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>ETag</ExposeHeader>
<MaxAgeSeconds>3600</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>
Path-style, the same request is PUT /my-bucket?cors on s3.eu-central-1.strg.com.
Addressing describes both styles.
Permissions
The access key needs Manage buckets (buckets:write), a permission that belongs to keys for
all buckets. A key without it, or a key limited to certain buckets, gets 403 AccessDenied.
Access keys has the details.
URI parameters
| Name | Type | Required | Description |
|---|---|---|---|
Bucket | String | Yes | The bucket whose CORS configuration is set. |
cors | Flag | Yes | Selects the CORS subresource. Sent without a value. |
STRG answers any other query parameter with 501 NotImplemented, so a client knows straight
away that the request was not what it meant.
Request headers
| Name | Type | Required | Description |
|---|---|---|---|
Content-MD5 | String | One integrity header is required | The base64 MD5 digest of the body. STRG checks it and answers 400 BadDigest if it does not match. |
x-amz-checksum-crc32, x-amz-checksum-crc32c, x-amz-checksum-crc64nvme, x-amz-checksum-sha1, x-amz-checksum-sha256 | String | One integrity header is required | A checksum of the body, which works in place of Content-MD5. |
x-amz-sdk-checksum-algorithm | String | No | The algorithm of the x-amz-checksum-* header the SDK sends. Accepted. |
x-amz-expected-bucket-owner | String | No | Welcome, and has nothing to check: an access key only ever reaches its own account's buckets. |
Every request carries either Content-MD5 or an x-amz-checksum-* header, and one with neither
gets 400 InvalidRequest. The AWS CLI and SDKs add one automatically.
The body arrives whole. STRG answers a streaming aws-chunked upload of the body with
501 NotImplemented; the AWS CLI and SDKs never send one for this operation.
Request body
A CORSConfiguration document of at most 2 MiB, holding 1 to 100 CORSRule elements. The
document is plain XML: a comment, CDATA section, DOCTYPE, processing instruction or namespace
prefix gets 400 MalformedXML.
| Element | Type | Required | Description |
|---|---|---|---|
CORSConfiguration | Container | Yes | The root element. |
CORSRule | Container | Yes | One rule, repeated for each. 1 to 100. |
AllowedOrigin | String | Yes, at least one per rule | An origin the rule allows. * for every origin, alone in its rule; https for every HTTPS origin; or http:// or https:// plus a host and an optional port, with no path and no trailing slash. The host may be *, or start with one *. label, as in https://*.example.com. Stored lowercased. |
AllowedMethod | String | Yes, at least one per rule | GET, HEAD, PUT, POST or DELETE, in capitals. Any other method gets 400 InvalidRequest. |
AllowedHeader | String | No | A request header the rule allows. * allows any and is then the only one; a trailing * matches a prefix, as in x-amz-*. Stored lowercased. |
ExposeHeader | String | No | A response header browsers may expose to scripts. Named in full, without *. Stored lowercased. |
MaxAgeSeconds | Integer | No | How long a browser may cache the preflight answer, in seconds. 0 to 86,400. |
ID | String | No | Accepted and set aside, so it is never returned. Configurations copied from elsewhere work as they are. |
Each origin, method and header appears once per rule; origins and headers compare
case-insensitively, so https://Example.com and https://example.com count as the same. Each
rule has a budget of 1,000 bytes, made up of a fixed overhead per rule and the length of every
origin, method and header - the rule size budget shows
how it is counted. An element not in this table gets 400 InvalidRequest, so a misspelt name
shows up straight away instead of being quietly dropped.
Response
A successful request answers 200 OK with no body. The new rules replace the old ones.
The rules apply on the S3 endpoint:
- A browser preflight (
OPTIONS) is answered from them. A preflight no rule allows - or for a bucket that does not exist - gets403 AccessForbidden. - Responses to an allowed origin carry the matching
Access-Control-*headers. - On a bucket with Bucket Protection off, an anonymous read may be answered from STRG's
cache, with the CORS headers of the rules in force when it was cached, for up to 5 minutes
after the change. STRG keeps no anonymous answer longer than that, and caches one only when
the object's own
Cache-Controlallows a shared cache to keep it. Anonymous reads has the details. - The STRG dashboard reaches your buckets whatever the rules say, so a rule set that leaves out the dashboard never locks you out of your own bucket.
Response headers
| Name | Description |
|---|---|
x-amz-request-id | The request ID, for support requests. |
Response body
None.
Errors
These are the errors this operation can answer, and the situation behind each.
| Code | HTTP status | When |
|---|---|---|
InvalidRequest | 400 | A rule goes past one of the limits above - its message names the rule and the field - or the document contains an element or method STRG does not take, or the request carries neither Content-MD5 nor an x-amz-checksum-* header. |
MalformedXML | 400 | The body is not well-formed XML, its root is not CORSConfiguration, it contains no CORSRule, or it uses a comment, CDATA section, DOCTYPE, processing instruction or namespace prefix. |
BadDigest | 400 | Content-MD5 does not match the body. |
XAmzContentSHA256Mismatch | 400 | x-amz-content-sha256 does not match the body. |
MaxMessageLengthExceeded | 400 | The body is larger than 2 MiB. |
NoSuchBucket | 404 | The bucket does not exist, it is in a region other than the endpoint's, or your account has no storage in this endpoint's region yet. |
AccessDenied | 403 | The access key lacks Manage buckets, is limited to certain buckets, or your account is not active. |
InvalidAccessKeyId | 403 | The access key does not exist, has expired or been revoked, or was created so recently that it is not yet usable in this endpoint's region - a moment later it is. |
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. |
NotImplemented | 501 | The request carries a query parameter this operation does not take, or sends the body as a streaming aws-chunked upload. |
SlowDown | 503 | Too many requests at once; a retry after the Retry-After interval goes through. |
ServiceUnavailable | 503 | A temporary failure; a retry usually succeeds. |
Differences from Amazon S3
A few things work a little differently on STRG, each listed here with what it means for your requests.
| Aspect | Amazon S3 | STRG |
|---|---|---|
| Permission | s3:PutBucketCORS, granted per identity | Manage buckets, on an access key for all buckets |
| Anonymous reads after a change | Not applicable | On a public bucket, cached anonymous answers can carry the old CORS headers for up to 5 minutes |
| Number of rules | Limited by a 64 KB document | Up to 100 rules |
| Size of a rule | None beyond the 64 KB document | Up to 1,000 bytes per rule, counted as the rule size budget describes |
MaxAgeSeconds | Any non-negative integer | 0 to 86,400 |
AllowedOrigin | Any string with at most one * | *, https, or http(s)://host[:port] with * as the whole host or its first label |
AllowedMethod | GET, PUT, HEAD, POST, DELETE | The same five |
Rule ID | Stored and returned | Set aside when the rules are saved |
| Case of origins and headers | Stored as sent | Lowercased |
Order of AllowedMethod | Stored as sent | Not guaranteed, so comparing methods as a set is the reliable check |
| Bucket in another region | 301 redirect with x-amz-bucket-region | 404 NoSuchBucket. Each bucket answers on its own regional endpoint. |
x-amz-expected-bucket-owner | Checked against the bucket owner | Welcome, with nothing to check: a key only ever reaches buckets of its own account |
Examples
Each example lets pages on https://www.example.com read and upload objects in my-bucket. The
CLI reads the rules from a file in the same JSON form the dashboard edits, wrapped in CORSRules:
{
"CORSRules": [
{
"AllowedOrigins": ["https://www.example.com"],
"AllowedMethods": ["GET", "HEAD", "PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
}
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api put-bucket-cors \
--bucket my-bucket \
--cors-configuration file://cors.json \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1
The command prints nothing on success.
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
s3.put_bucket_cors(
Bucket="my-bucket",
CORSConfiguration={
"CORSRules": [
{
"AllowedOrigins": ["https://www.example.com"],
"AllowedMethods": ["GET", "HEAD", "PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600,
}
]
},
)
import { PutBucketCorsCommand, 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 PutBucketCorsCommand({
Bucket: "my-bucket",
CORSConfiguration: {
CORSRules: [
{
AllowedOrigins: ["https://www.example.com"],
AllowedMethods: ["GET", "HEAD", "PUT"],
AllowedHeaders: ["*"],
ExposeHeaders: ["ETag"],
MaxAgeSeconds: 3600,
},
],
},
}),
);
Reading the rules back with GetBucketCors returns etag rather than
ETag, because header names are stored lowercased.
When a rule goes past a limit, nothing is saved and the bucket keeps its previous rules. The error
message names the rule and the field - here, for a MaxAgeSeconds of 90000 in the first rule:
An error occurred (InvalidRequest) when calling the PutBucketCors operation: Invalid CORS configuration - Rule 1, MaxAgeSeconds: at most 86400 seconds (one day).