# 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.

Source: https://www.coritan.com/docs/object-storage/cors/

In the dashboard:

- /dashboard/storage/…/buckets/…/settings: https://www.coritan.com/dashboard/storage

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.

## Before you begin

- Sign in to the [dashboard](https://www.coritan.com/dashboard/storage) 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 {#how-it-answers}

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 own rule {#dashboard-rule}

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](#read-with-s3)). You cannot give a rule of your own that ID.

## Add a rule

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.`

## Result

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.

## Change or delete a rule

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](#how-it-answers)).

## Edit every rule as JSON

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 {#read-with-s3}

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

```bash
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](/docs/object-storage/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](#with-the-api)).

## Troubleshooting

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](/docs/billing/failed-payments/)), 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](/docs/support/conversations/) with the whole message.

## Related

- [Delete objects on a schedule with lifecycle rules](/docs/object-storage/lifecycle-rules/)
- [Connect an S3 client](/docs/object-storage/connect-an-s3-client/)
- [S3 API compatibility](/docs/object-storage/s3-compatibility/)
- [Object Storage limits](/docs/object-storage/limits/)

## With the API

Each request takes the service ID and the bucket ID, from [`GET /api/v1/client/object-storage/{service_id}/buckets`](/docs/api/reference/client/object-storage/object-storage-buckets/#op-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`.

### Read the rules

[`GET /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors`](/docs/api/reference/client/object-storage/object-storage-buckets/#op-get-api-v1-client-object-storage-service-id-buckets-bucket-id-cors) returns the bucket's rules in the order browsers check them:

```bash
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.

### Replace the rules

[`PUT /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors`](/docs/api/reference/client/object-storage/object-storage-buckets/#op-put-api-v1-client-object-storage-service-id-buckets-bucket-id-cors) replaces every rule with the list you send:

```bash
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.

### Remove every rule

[`DELETE /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors`](/docs/api/reference/client/object-storage/object-storage-buckets/#op-delete-api-v1-client-object-storage-service-id-buckets-bucket-id-cors) removes them all and answers with an empty `rules` list:

```bash
curl -X DELETE https://api.coritan.com/api/v1/client/object-storage/1207/buckets/31/cors \
  -H "Authorization: Bearer $CORITAN_TOKEN"
```

### Errors

| 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](#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

- `GET /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors`: Get bucket cors (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-buckets/#op-get-api-v1-client-object-storage-service-id-buckets-bucket-id-cors)
- `PUT /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors`: Replace the bucket's CORS rules with the ones sent, at most 99 (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-buckets/#op-put-api-v1-client-object-storage-service-id-buckets-bucket-id-cors)
- `DELETE /api/v1/client/object-storage/{service_id}/buckets/{bucket_id}/cors`: Remove every CORS rule from the bucket (https://www.coritan.com/docs/api/reference/client/object-storage/object-storage-buckets/#op-delete-api-v1-client-object-storage-service-id-buckets-bucket-id-cors)
