Skip to content
Coritan Docs

Let websites use a bucket with CORS rules

Choose which websites may call a bucket from a browser, with which methods and headers, and read the rules back with any S3 client.

View as Markdown

A CORS rule tells browsers which other websites may use a bucket. When a page on https://example.com uploads to a bucket or reads from it with JavaScript, the browser first asks the bucket whether that site may. The bucket answers from its CORS rules, and the browser blocks the request when no rule allows it.

You set CORS rules on the CORS card in the bucket's Settings tab, or with the API. S3 clients can read the rules and cannot change them.

  • Sign in to the dashboard and open the service. The service must be active to change rules. While it waits for payment or is suspended, you can read them.
  • Know each site's origin: its scheme, its host and its port when it is not the usual one, such as https://example.com or https://app.example.com:8443. An origin has no path and no trailing slash.

How the bucket answers browsers

Section titled How the bucket answers browsers

The rules apply at the bucket's S3 endpoint, such as https://s3.fra.coritan.com:7337/u7-assets. If the bucket is public, they also apply at its public URL and custom domains.

With no rules:

  • Pages on any website can send signed or presigned requests to the S3 endpoint from a browser. Amazon S3 and Cloudflare R2 refuse these requests until a bucket has rules.
  • The public URL and custom domains answer no other website. A page elsewhere can still show an image or play a video from them, but it cannot read an object with fetch.

With rules:

  • A browser request uses the first rule that matches its origin, its method and every header it sends. When no rule matches, the browser blocks the request.
  • Each request to the S3 endpoint still needs a signature or a presigned URL. A rule only lets the browser send it.
  • A public URL or custom domain answers GET and HEAD only, whatever the rule allows.

Changes apply within a minute.

The dashboard's file browser uploads to your bucket from https://www.coritan.com. So that it keeps working once you add rules, we put one more rule in front of yours. Its ID is coritan-dashboard, it allows https://coritan.com and https://www.coritan.com, and it does not appear on the CORS card.

S3 holds at most 100 CORS rules on a bucket, and this rule takes one of them, so you can add up to 99. An S3 client that reads the bucket's CORS sees coritan-dashboard first (Read the rules with an S3 client). You cannot give a rule of your own that ID.

  1. Open the service, select the Buckets tab, then select the bucket.
  2. Select the Settings tab. The CORS card lists the bucket's rules.
  3. Select Add rule….
  4. In Allowed origins, type each origin on a line of its own, such as https://example.com. A * alone allows every website. One * in a host allows its subdomains, as in https://*.example.com.
  5. Under Allowed methods, tick what pages on those sites may do. GET and HEAD read, PUT and POST upload, and DELETE removes.
  6. In Allowed headers, list the request headers a page may send, one per line, such as content-type. One * in a name matches many headers, as in x-amz-meta-*, and * alone allows every header. A browser upload usually sends content-type, and an S3 library also sends authorization and several x-amz- headers, so * is the simplest choice.
  7. In Exposed headers, list the response headers your JavaScript needs to read, such as ETag. A multipart upload from a browser needs ETag. This field does not take *.
  8. Optionally, set Max age in seconds. A browser keeps the answer to its preflight check for that long and does not ask again for each request.
  9. Optionally, give the rule a Rule ID to tell it apart from the others.
  10. Select Add rule.

A message confirms it: CORS rule added. It applies within a minute.

The rule appears on the CORS card with its Origins, Methods and Headers, and the card says how many rules the bucket has out of 99. Rules are listed in the order browsers check them, and a new rule goes last.

To check a rule, open your site and repeat the request. In the browser's developer tools, the request's preflight (an OPTIONS request) answers with Access-Control-Allow-Origin set to your origin, or to * for a rule that allows every website.

Each rule's menu has Edit rule… and Delete rule…. Edit rule… opens the same fields as Add rule…; select Save rule to keep the change. Delete rule… asks you to confirm with Delete rule.

Deleting the last rule takes the bucket back to having no rules (How the bucket answers browsers).

You can paste rules written for Amazon S3 or Cloudflare R2.

  1. On the CORS card, open the menu at the top right and select Edit as JSON….
  2. In CORS rules, paste the rules. The box takes:
    • S3's CORS XML, starting with <CORSConfiguration>.
    • JSON as aws s3api get-bucket-cors prints it, with a CORSRules list.
    • R2's CORS JSON, with allowed and exposeHeaders in each rule.
  3. Select Save rules.

Saving replaces every rule on the bucket, and an empty box removes them all. When the pasted rules include the coritan-dashboard rule, we leave it out, since the dashboard keeps its own.

To remove every rule at once, select Remove every rule… in the same menu and confirm with Remove rules.

Read the rules with an S3 client

Section titled Read the rules with an S3 client

Any access key whose scope covers the bucket can read its CORS rules, read only keys included. With the AWS CLI:

Shell
aws s3api get-bucket-cors --bucket u7-assets \
  --endpoint-url https://s3.fra.coritan.com:7337
JSON
{
  "CORSRules": [
    {
      "ID": "coritan-dashboard",
      "AllowedHeaders": ["*"],
      "AllowedMethods": ["GET", "PUT", "HEAD", "DELETE"],
      "AllowedOrigins": ["https://coritan.com", "https://www.coritan.com"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3000
    },
    {
      "ID": "web-app",
      "AllowedHeaders": ["content-type"],
      "AllowedMethods": ["GET", "PUT"],
      "AllowedOrigins": ["https://example.com"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3600
    }
  ]
}

A bucket with no rules answers NoSuchCORSConfiguration. Each read is a Class B request (Usage and billing).

PutBucketCors and DeleteBucketCors answer 403 AccessDenied with every access key. Change the rules in the dashboard or with the API (With the API).

The browser console says the request was blocked by CORS policy
No rule matched the request. Check that a rule lists the page's exact origin, scheme and port included, the method, and every header the page sends. A rule for https://example.com does not cover https://www.example.com. For a public URL or custom domain, the bucket needs a rule even for a plain GET.
CORS rule 1: https://example.com/app is not an origin. Write a scheme and a host, such as https://example.com, with no path or trailing slash.
Remove the path and any trailing slash from the origin. The number says which rule is at fault.
CORS rule 1: PATCH is not a method a rule can allow. Use GET, PUT, POST, DELETE or HEAD.
A pasted or API rule names a method S3 does not offer. Remove it.
CORS rule 1: an exposed header cannot use *. Name each header.
List each response header your page reads, such as ETag.
CORS rule 1: the ID coritan-dashboard belongs to the dashboard. Pick another.
Give the rule another ID. In the API, leave the dashboard's rule out of the list you send.
A bucket can have at most 99 CORS rules. The dashboard keeps one more so its file browser can upload.
Merge rules that allow the same methods and headers into one rule with several origins. Add rule… is greyed out while the bucket has 99.
These CORS rules are too long. Keep them under 64 KB in all.
Use fewer rules, or a * in an origin or header to cover several at once.
This is not JSON or XML. Paste the rules as S3 writes them.
The text in Edit as JSON… is neither. Paste the whole document, including its first {, [ or <.
This service is suspended; it can be changed once it is active
Pay the overdue invoice (Failed payments and suspended services), then try again. The message names the status pending while the service waits for payment.
Too many requests for this action. Try again later.
Your account made 60 CORS and lifecycle rule changes in the last hour. Wait, then try again.
The bucket would not take these CORS rules. Try again in a minute.
The bucket's region could not save the rules, and nothing changed. Try again. If it keeps happening, contact support with the whole message.

Each request takes the service ID and the bucket ID, from GET /api/v1/client/object-storage/{service_id}/buckets. A service that is not Object Storage on your account answers 404 with Object storage service not found, and a bucket that is not on the service answers 404 with Bucket not found.

GET /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors returns the bucket's rules in the order browsers check them:

Shell
curl https://api.coritan.com/api/v1/client/object-storage/1207/buckets/31/cors \
  -H "Authorization: Bearer $CORITAN_TOKEN"
JSON
{
  "bucket_id": 31,
  "rules": [
    {
      "id": "web-app",
      "allowed_origins": ["https://example.com"],
      "allowed_methods": ["GET", "PUT"],
      "allowed_headers": ["content-type"],
      "expose_headers": ["ETag"],
      "max_age_seconds": 3600
    }
  ],
  "max_rules": 99
}

The list never holds the dashboard's rule. max_rules is how many rules the bucket can have.

PUT /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors replaces every rule with the list you send:

Shell
curl -X PUT https://api.coritan.com/api/v1/client/object-storage/1207/buckets/31/cors \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"rules": [{"id": "web-app", "allowed_origins": ["https://example.com", "https://*.example.com"], "allowed_methods": ["GET", "PUT"], "allowed_headers": ["content-type"], "expose_headers": ["ETag"], "max_age_seconds": 3600}]}'

It answers 200 with the rules as saved, in the shape above. We write methods in capitals and drop repeated values. id, allowed_headers, expose_headers and max_age_seconds are optional. An empty rules list removes every rule.

DELETE /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors removes them all and answers with an empty rules list:

Shell
curl -X DELETE https://api.coritan.com/api/v1/client/object-storage/1207/buckets/31/cors \
  -H "Authorization: Bearer $CORITAN_TOKEN"
Status detail Cause
409 This service is pending; it can be changed once it is active The service is not active. The message names its status.
422 A message that names the rule, such as CORS rule 2: PATCH is not a method a rule can allow. Use GET, PUT, POST, DELETE or HEAD. A rule breaks S3's rules. Troubleshooting lists the common ones.
422 A list of fields The body is not in the shape above.
429 An object with "error": "rate_limited" and retry_after_seconds Your account made 60 CORS and lifecycle rule changes in the last hour. The Retry-After header says how long to wait.
502 The bucket would not take these CORS rules. Try again in a minute. The bucket's region refused the rules. Nothing changed.

API operations on this page

MethodPathWhat it does
GET/api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/corsGet bucket cors
PUT/api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/corsReplace the bucket's CORS rules with the ones sent, at most 99
DELETE/api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/corsRemove every CORS rule from the bucket