> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CORS

> Control which browsers can call your API across origins with the environment-configured CorsMiddleware.

**Cross-Origin Resource Sharing (CORS)** is the browser security mechanism that decides whether a page served from one origin (scheme + host + port) may read a response from another. Without the right response headers, the browser blocks the cross-origin read even though your server handled the request. The `@ooneex/middleware` component ships a ready-to-use `CorsMiddleware` that reads its policy from environment variables, validates the request's `Origin`, sets the matching `Access-Control-*` headers, and answers preflight (`OPTIONS`) requests — so you turn CORS on by configuration, not code.

## Installation

`CorsMiddleware` ships with `@ooneex/middleware`, which is already a dependency of `@ooneex/app`. If you depend on it directly, add it:

```bash theme={null}
bun add @ooneex/middleware
```

It is constructed with `AppEnv` from `@ooneex/app-env` injected by the container, so its policy is read from the same configuration your app already loads — there is nothing else to wire up.

## Why this component

* **Zero-code policy.** The whole policy comes from `CORS_*` environment variables read through `AppEnv`; you register the middleware once and tune it per environment with no redeploy of code.
* **Origin-checked.** Every request's `Origin` is matched against your allowlist before any header is set. A disallowed origin gets no CORS headers, so the browser blocks it.
* **Preflight handled.** `OPTIONS` requests are answered with `Access-Control-Max-Age` and a `204`, so browsers cache the preflight and skip it on later calls.
* **Sensible defaults.** Methods, headers, and a one-day max-age default to safe values; you only set what you need to change.
* **Container-native.** It is a normal injectable middleware resolved from the container — register it through `App`'s dedicated `cors` slot, alongside your [auth](/security/jwt) pipeline.

## How it works

The middleware reads its configuration once at construction from the injected `AppEnv`, then applies it on every request in `handler`.

| Stage        | What happens                                                                                                                                    |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Construction | `AppEnv` is injected; the `CORS_*` variables are parsed into origins, methods, headers, exposed headers, a credentials flag, and a max-age.     |
| No `Origin`  | If the request has no `Origin` header (a same-origin or non-browser call), the context is returned untouched — no CORS headers.                 |
| Origin check | The `Origin` is matched against the allowlist. If it is not allowed, the context is returned untouched.                                         |
| Allowed      | `Access-Control-Allow-Origin`, `-Allow-Methods`, `-Allow-Headers`, and `-Allow-Credentials` are set; exposed headers are added when configured. |
| Preflight    | On `OPTIONS`, `Access-Control-Max-Age` is set and the middleware responds `204`.                                                                |

When `CORS_ORIGINS` is `*` (the default), the allow-origin header is sent as the literal `*`. When you configure a specific allowlist, the middleware reflects the **request's** origin back instead of a list — the only spec-compliant way to allow several named origins.

## Configuration

Every setting is an environment variable read through `AppEnv`. List values are comma-separated.

| Variable               | Default                               | Description                                                                                                                               |
| ---------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `CORS_ORIGINS`         | `*`                                   | Allowed origins. `*` allows any origin; otherwise a comma-separated allowlist, e.g. `https://app.example.com, https://admin.example.com`. |
| `CORS_METHODS`         | `GET, HEAD, PUT, PATCH, POST, DELETE` | Methods sent in `Access-Control-Allow-Methods`.                                                                                           |
| `CORS_HEADERS`         | `Content-Type, Authorization`         | Request headers allowed via `Access-Control-Allow-Headers`.                                                                               |
| `CORS_EXPOSED_HEADERS` | *(none)*                              | Response headers the browser may read, sent as `Access-Control-Expose-Headers`. Omitted when unset.                                       |
| `CORS_CREDENTIALS`     | `false`                               | When `true`, sets `Access-Control-Allow-Credentials: true` so cookies and auth headers are allowed.                                       |
| `CORS_MAX_AGE`         | `86400`                               | Seconds a browser may cache the preflight result, sent as `Access-Control-Max-Age` on `OPTIONS`.                                          |

In an Ooneex app these are set in `.env.yml` under the `cors` key, which `AppEnv` maps onto the `CORS_*` variables when the app loads its environment:

```yaml .env.yml theme={null}
# Cross-Origin Resource Sharing
cors:
  # Allow two named front-ends to call the API with credentials.
  origins: "https://app.example.com, https://admin.example.com"
  methods: "GET, POST, PUT, DELETE"
  headers: "Content-Type, Authorization"
  exposed_headers: "X-Request-Id, X-Total-Count"
  credentials: "true"
  max_age: "3600"
```

The same values can be supplied as plain environment variables instead — `AppEnv` reads whichever is present:

```bash .env theme={null}
CORS_ORIGINS=https://app.example.com, https://admin.example.com
CORS_METHODS=GET, POST, PUT, DELETE
CORS_HEADERS=Content-Type, Authorization
CORS_EXPOSED_HEADERS=X-Request-Id, X-Total-Count
CORS_CREDENTIALS=true
CORS_MAX_AGE=3600
```

<Warning>
  Do not combine `CORS_CREDENTIALS=true` with `CORS_ORIGINS=*`. Browsers reject
  a credentialed response whose allow-origin is the wildcard `*`. When you need
  credentials, set an explicit allowlist so the middleware reflects a concrete
  origin instead.
</Warning>

## Registering the middleware

`App` takes a dedicated `cors` slot in its config — pass `CorsMiddleware` there rather than listing it among your route `middlewares`:

```typescript theme={null}
import { App } from "@ooneex/app";
import { CorsMiddleware } from "@ooneex/middleware";
import { TerminalLogger } from "@ooneex/logger";
import { AuthMiddleware } from "./middleware";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger],
  middlewares: [AuthMiddleware],
  cors: CorsMiddleware,
});

await app.run();
```

The `cors` slot is wired specially. The framework appends it after your route middlewares **and** runs it on the catch-all (`/*`) handler, so CORS headers land on every response — including `404`s and routes short-circuited by [authentication](/security/jwt), since the pipeline runs the whole chain without breaking. That is why CORS belongs in `cors`, not in `middlewares`: a middleware listed only in `middlewares` never runs on unmatched routes.

With nothing configured, registering `CorsMiddleware` already gives you a permissive `*` policy — useful in development. Lock it down per environment by setting the `cors.*` keys in `.env.yml` (or the `CORS_*` variables).

## How a request flows

The middleware only acts when the browser sends an `Origin` header, and only when that origin is allowed.

```typescript theme={null}
// Inside CorsMiddleware.handler — illustrative.
const origin = context.header.get("Origin");

// Same-origin or non-browser call: nothing to do.
if (!origin) return context;

// Origin not in the allowlist: send no CORS headers, the browser blocks the read.
if (!this.isOriginAllowed(origin)) return context;

// Allowed: reflect the origin (or "*") and advertise the policy.
context.response.header
  .setAccessControlAllowOrigin(this.origins === "*" ? "*" : origin)
  .setAccessControlAllowMethods(this.methods)
  .setAccessControlAllowHeaders(this.headers)
  .setAccessControlAllowCredentials(this.credentials);

// Preflight: cache the result and answer 204.
if (context.method === "OPTIONS") {
  context.response.header.set("Access-Control-Max-Age", String(this.maxAge));
  context.response.json({}, 204);
}

return context;
```

A disallowed origin is not an error response — the request still runs, but without CORS headers the **browser** refuses to expose the result to the page. That is by design: CORS is enforced client-side.

## Preflight requests

For any "non-simple" request — a custom header, or a method beyond `GET`/`HEAD`/`POST` — the browser first sends an `OPTIONS` preflight. `CorsMiddleware` answers it with the allow headers plus `Access-Control-Max-Age`, then a `204`. The browser caches that result for `CORS_MAX_AGE` seconds and skips the preflight on subsequent identical calls, so raising `CORS_MAX_AGE` reduces round-trips at the cost of slower policy changes taking effect.

## Best practices

* **Pin origins in production.** Use an explicit `CORS_ORIGINS` allowlist rather than `*` for any deployed environment; reserve the wildcard for local development.
* **Only enable credentials when needed.** Set `CORS_CREDENTIALS=true` solely for cookie- or auth-header-based flows, and always pair it with a named-origin allowlist.
* **Keep the allowed headers tight.** List only the request headers your API actually reads in `CORS_HEADERS`; a broad list weakens the intent of the policy.
* **Use the `cors` slot, not `middlewares`.** Passing `CorsMiddleware` as `cors` is what makes it run on the catch-all and on rejected requests, so the browser can read errors and `404`s carry the right headers.
* **Tune max-age deliberately.** A longer `CORS_MAX_AGE` cuts preflight traffic but means origin or method changes take longer to reach already-cached browsers.

## Related

* [Middleware](/components/middleware) — the pipeline `CorsMiddleware` runs in.
* [JWT](/security/jwt) — token verification middleware to register after CORS.
* [Users](/security/users) — the user model resolved on authenticated requests.
* [Configuration](/getting-started/configuration) — how `AppEnv` loads the `CORS_*` variables.
