# CORS

> Let pages on other origins call your App from a browser, with or without cookies, and know what the App refuses and why.

This guide covers Cross-Origin Resource Sharing (CORS) in Tinka. You will learn how to:

- Let a front end on another origin call your App
- Send cookies and `Authorization` from that front end
- Allow every subdomain, and every port on `localhost`
- Choose the methods and headers other origins may use
- Let pages read your response headers
- Serve a public API that any site can read
- Understand how the App answers preflights, and what it refuses at startup

## Overview

A browser keeps a page from reading responses that come from another **origin**: another scheme, host or port. A front end at `https://app.example.com` that calls an API at `https://api.example.com` is cross-origin, and so is a dev server at `http://localhost:5173` that calls an App at `http://localhost:3000`. Without that rule, any site a user visits could read their data from every other site they're signed in to.

**CORS** is how a server tells the browser which other origins may read its responses. Before a request that a plain HTML form couldn't send, such as a `DELETE` or a JSON body, the browser first asks with an `OPTIONS` request, the **preflight**, and sends the real request only if the answer allows it.

A Tinka App allows no other origin until you list one in `HTTPConfig.CORS`. The App answers preflights itself, before any middleware runs, so authentication middleware never refuses them. It adds the CORS headers to every response, errors included, so a page can read why a request failed.

## Allow an origin

List the origins of your front ends in `Origins`, written as the browser sends them: the scheme, the host in lower case, and a port only when it isn't the default one:

```go title="config/http.go" ins={21-23}
package config

import (
	"strconv"

	"github.com/mucolabs/tinka"
	"github.com/mucolabs/tinka/env"
)

func httpConfig(e *env.Env) tinka.HTTPConfig {
	return tinka.HTTPConfig{
		// Addr is where the App listens. act serve listens for it in
		// development.
		Addr: ":" + strconv.Itoa(e.Int("PORT", 3000)),

		// Debug shows unexpected errors' messages and stacks in responses.
		// It's off unless APP_DEBUG=true, which .env sets on your machine:
		// production reads no .env file, so it stays off there even when
		// APP_ENV is forgotten.
		Debug: e.Bool("APP_DEBUG", false),
		CORS: tinka.CORSConfig{
			Origins: []string{"https://app.example.com"},
		},
	}
}
```

Pages from `https://app.example.com` can now call the App with the methods `GET`, `HEAD`, `POST`, `PUT`, `PATCH` and `DELETE`, send any header a page may set, such as `Content-Type: application/json` and `Authorization`, and read the responses. Other origins still can't.

A listed origin may also send requests that change data. The App refuses `POST`, `PUT`, `PATCH` and `DELETE` from another origin's pages, to stop cross-site request forgery, and trusts the origins you list here.

### Read the origins from the environment

Front ends often live at different addresses in staging and production. Read them from a variable that lists origins separated by spaces, with `strings.Fields` (add `"strings"` to the file's imports):

```go title="config/http.go" ins={2}
		CORS: tinka.CORSConfig{
			Origins: strings.Fields(e.String("CORS_ORIGINS", "")),
		},
```

```sh title=".env"
CORS_ORIGINS=http://localhost:5173
```

An empty or missing variable lists no origins, which turns CORS off.

## Send cookies

A page's request carries no cookies to another origin, and its page can't read a response that set any, unless the server allows **credentials**. Turn `Credentials` on for a front end that signs in with a session cookie:

```go title="config/http.go" ins={3}
		CORS: tinka.CORSConfig{
			Origins:     []string{"https://app.example.com"},
			Credentials: true,
		},
```

The front end must ask for them too, with `credentials: "include"` in `fetch`. A front end that sends a token in `Authorization` doesn't need credentials: the header is allowed without them.

With credentials, every origin must use HTTPS, except on `localhost`. Over plain HTTP, anyone on the network between the user and that origin could serve their own page as it, and act with the user's cookies. The App refuses to start with such an origin.

## Allow subdomains and local ports

Two patterns cover origins you can't list one by one:

::::options

:::option{name="https://*.example.com" type="any subdomain"}
Every subdomain of `example.com`, at any depth, such as the preview deployment `https://pr-42.preview.example.com`. `example.com` itself isn't included: list it too when it serves a front end. Only the first label can be `*`, and the domain must be yours: a pattern over a public suffix, such as `*.com`, `*.co.uk` or `*.github.io`, would match other people's sites, and the App refuses to start with one.
:::

:::option{name="http://localhost:*" type="any port"}
Every port on a loopback host, for dev servers that pick a free port. Only `localhost`, its subdomains and loopback addresses can use `:*`.
:::

::::

```go title="config/http.go"
		CORS: tinka.CORSConfig{
			Origins: []string{"https://app.example.com", "https://*.preview.example.com", "http://localhost:*"},
		},
```

## Choose methods and headers

Browsers always allow `GET`, `HEAD` and `POST`. `Methods` lists the other methods pages may use, and defaults to `PUT`, `PATCH` and `DELETE`. Method names are case-sensitive, so list them as your front end sends them. `fetch` turns `delete` and `put` into upper case, but sends `patch` as written:

```go title="config/http.go" ins={3}
		CORS: tinka.CORSConfig{
			Origins: []string{"https://app.example.com"},
			Methods: []string{"PUT", "DELETE"},
		},
```

`Headers` lists the request headers pages may send. It defaults to any header a page may set; list them to allow only those. Case doesn't matter:

```go title="config/http.go" ins={3}
		CORS: tinka.CORSConfig{
			Origins: []string{"https://app.example.com"},
			Headers: []string{"Content-Type", "Authorization"},
		},
```

A JSON body needs `Content-Type`: only form and plain text bodies go without asking.

## Expose response headers

A page reads only a few response headers from another origin, such as `Content-Type`. `ExposeHeaders` lists the others it may read:

```go title="config/http.go" ins={3}
		CORS: tinka.CORSConfig{
			Origins:       []string{"https://app.example.com"},
			ExposeHeaders: []string{"X-Request-Id", "Location"},
		},
```

## Cache preflights

A browser reuses the answer to a preflight for `MaxAge`, so later requests skip the extra round trip. It defaults to two hours, the longest Chromium honors. Firefox honors up to 24 hours, the most `MaxAge` accepts. A negative value makes browsers ask before every request, which helps while you change the configuration:

```go title="config/http.go" ins={3}
		CORS: tinka.CORSConfig{
			Origins: []string{"https://app.example.com"},
			MaxAge:  -1,
		},
```

## Serve a public API

`"*"` lets every origin read the App's responses, for public data such as an open API or a status page. Every response says so with `Access-Control-Allow-Origin: *`:

```go title="config/http.go"
		CORS: tinka.CORSConfig{
			Origins: []string{"*"},
		},
```

`"*"` stands alone, and can't be used with `Credentials`: browsers refuse that combination, because every site could then read what your users see.

:::warning
`"*"` allows reading, not writing. Requests that change data from other sites' pages are still refused with `403`, because with `"*"` the App can't tell your front end from a site that tries to act with your users' cookies.

List your own front ends instead of `"*"` when they send requests that change data:

```go title="config/http.go" del={2} ins={3}
		CORS: tinka.CORSConfig{
			Origins: []string{"*"},
			Origins: []string{"https://app.example.com"},
		},
```
:::

## How the App answers

A preflight is an `OPTIONS` request with `Origin` and `Access-Control-Request-Method` headers. The App answers it before any middleware runs:

- When the origin, the method and every header it asks about are allowed, with `204 No Content` and the headers that allow them.
- Otherwise, with `403 Forbidden` and the code `cors_refused`, and no CORS headers, so the browser stops and tells the page nothing about the configuration. The browser's console says the request was blocked by CORS policy, and the App logs the refusal, with its reason, at the warning level.

Any other `OPTIONS` request reaches the router as usual.

Every other response carries `Vary: Origin`, because whether it allows the page to read it depends on the `Origin` header. Caches then keep one copy per origin, instead of handing one origin's answer to another. A handler that varies its response on another header adds to it with `c.Header().Add("Vary", "Accept")`; `Set` would remove `Origin`.

## Mistakes the App refuses

The App checks the configuration when it's created, and panics on a mistake that would never work in a browser or would open the App to every site. The message names the origin or the value, and how to write it:

| Mistake | Why |
|---|---|
| `"*"` with `Credentials`, or `"*"` with other origins | Browsers refuse it; `"*"` already allows every origin |
| `"null"` | Browsers send `null` from sandboxed pages, which any site can create |
| A path or a trailing `/`, such as `https://app.example.com/` | An origin is only a scheme, a host and a port |
| Upper case, a default port such as `:443`, or a host not in Punycode | Browsers never send them, so the origin would never match |
| `http://` with `Credentials`, other than on `localhost` | Anyone on the network could act as the page |
| A wildcard over a public suffix, such as `https://*.github.io` | It matches other people's sites |
| `CONNECT` or `TRACE` in `Methods`, `Cookie` or `Sec-*` in `Headers`, `Set-Cookie` in `ExposeHeaders` | Browsers never send or expose them |
| A `MaxAge` over 24 hours, or not in whole seconds | Browsers cap it, and send whole seconds |

See also: [Security headers](security-headers.md), [Middleware](../basics/middleware.md), [App reference](../../reference/app.md#what-every-request-gets), [Configuration](../../configuration.md)
