---
title: CORS
description: The CORS middleware and configuration schema — a global policy plus per-route overrides, enforced in the gateway's request chain.
---

# CORS

Octopus ships a complete Cross-Origin Resource Sharing (CORS) middleware
(`octopus_middleware::Cors`) that handles preflight `OPTIONS` requests and adds
`Access-Control-*` headers to responses. It is configured globally (top-level
`cors`) with optional per-route overrides (`routes[].cors`).

<Callout type="info" title="How CORS is enabled">
  Setting the top-level **`cors`** block adds the CORS middleware to the request
  chain. Once enabled, a matched route's **`routes[].cors`** override (attached as
  the `MatchedRouteCors` request extension) replaces the global policy for that
  route. If no top-level `cors` block is configured, the middleware is **not**
  added — and per-route `cors` has no effect — so set a (possibly restrictive)
  global `cors` block to turn CORS handling on.
</Callout>

## What the middleware does

The `Cors` middleware:

- Resolves an effective config: a per-route `MatchedRouteCors` override if present
  in request extensions, otherwise the global configured policy.
- For `OPTIONS` preflight: returns `204 No Content` with
  `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`,
  `Access-Control-Allow-Headers`, `Access-Control-Max-Age`, and (if enabled)
  `Access-Control-Allow-Credentials`.
- For other requests: runs the inner chain, then adds
  `Access-Control-Allow-Origin` (and exposed-headers / credentials headers) to the
  response.

### Origin handling

- If `allowed_origins` contains `"*"` and credentials are **not** allowed, the
  response uses `Access-Control-Allow-Origin: *`.
- If `allowed_origins` contains `"*"` and credentials **are** allowed, the
  request's own `Origin` is echoed back (since `*` is invalid with credentials).
- Otherwise the request `Origin` is echoed only when it is in `allowed_origins`.

## Configuration schema

### Global: top-level `cors`

Setting this block enables CORS handling for the whole gateway.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `allowed_origins` | list&lt;string&gt; | `[]` | Allowed origins (`"*"` for any) |
| `allowed_methods` | list&lt;string&gt; | `[GET, POST, PUT, DELETE, OPTIONS]` | Allowed methods |
| `allowed_headers` | list&lt;string&gt; | `[]` | Allowed request headers |
| `exposed_headers` | list&lt;string&gt; | `[]` | Headers exposed to the browser |
| `max_age` | int (seconds) | `3600` | Preflight cache lifetime |
| `allow_credentials` | bool | `false` | Allow cookies / `Authorization` |

```yaml
cors:
  allowed_origins:
    - https://app.example.com
  allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]
  allowed_headers: [Content-Type, Authorization]
  exposed_headers: [X-Request-ID]
  max_age: 3600
  allow_credentials: false
```

### Per-route: `routes[].cors`

Overrides the global policy for a matched route (requires the global `cors` block
to be set, which is what adds the middleware to the chain).

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `allowed_origins` | list&lt;string&gt; | `[]` | Allowed origins (`"*"` for any) |
| `allowed_methods` | list&lt;string&gt; | `[]` | Allowed HTTP methods |
| `allowed_headers` | list&lt;string&gt; | `[]` | Allowed request headers |
| `allow_credentials` | bool | `false` | Allow cookies / `Authorization` |
| `max_age` | int (seconds) | `3600` | Preflight cache lifetime |

```yaml
routes:
  - path: /api/*
    upstream: backend
    methods: [GET, POST, OPTIONS]
    cors:
      allowed_origins:
        - https://app.example.com
      allowed_methods: [GET, POST]
      allowed_headers: [Content-Type, Authorization]
      allow_credentials: true
      max_age: 3600
```

## Related

- [Routes](/docs/octopus/configuration/routes) — where `routes[].cors` lives.
- [Authentication & authorization](/docs/octopus/configuration/auth) — the top-level `cors` schema reference.
- [Middleware overview](/docs/octopus/middleware) — the full request-chain order.
