CORS Header Generator
Emit a complete CORS configuration for nginx, Apache, Express or Caddy — preflight, Vary: Origin and credentials handled — or read the headers you already have.
Generate writes a complete CORS block for one server. Explain reads response headers you already have and says what each one does. Both run on this one page — the link you share carries the mode with it.
# CORS — nginx (ngx_http_headers_module + ngx_http_map_module).
# The map block belongs in the http context, above your server blocks.
map $http_origin $cors_allow_origin {
default "";
"https://app.example.com" $http_origin;
}
# Place this location inside the server block it applies to.
location /api/ {
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type" always;
add_header Access-Control-Max-Age "600" always;
add_header Vary "Origin" always;
if ($request_method = OPTIONS) {
return 204;
}
}nginx · 686 B
Directives this page can emit
- map $http_origin
- add_header … always
- if ($request_method = OPTIONS)
- return 204
The whole set of directives this page will ever write for the selected server; a given block uses the part your policy needs. These four servers are the whole declared coverage of this page — no other server's syntax is emitted or guessed.
This page writes configuration text and reads headers you paste. It never sends a request to your endpoint and never reports whether a live request would pass.
Press ? for shortcuts
Do it offline:$
How to use it
- Start in Generate and choose between allowing every origin and listing the ones you trust. Each entry takes the form a browser sends: scheme, host, and a port when it is not the default.
- Tick the methods your routes answer, then name the request fields your own code sets. Anything the JavaScript reads back off the response belongs in the expose list instead.
- Switch credentials on only when cookies or an Authorization field ride along. Paired with the wildcard origin it is refused outright, because no config file can make that pair work.
- Pick the server tab. Each block arrives with a comment saying where it belongs, its directives cited underneath, and a note for the trap that server is known for.
- Copy, download or share the block — the whole form travels in the URL fragment. Paste a response header block into Explain to read a deployment back the other way.
Describe the origins, methods and header names your API really needs and this page writes the configuration block for nginx, Apache 2.4, Express or Caddy 2 — the OPTIONS short-circuit, Vary: Origin and the credentials rule already handled. Paste a response header block instead and each Access-Control field is read back in plain words. Both directions run on this device.
Examples
Allow every origin · GET, HEAD, OPTIONS · Max-Age 600 · the server answers the preflight · nginx# CORS — nginx (ngx_http_headers_module + ngx_http_map_module).
# Place this location inside the server block it applies to.
location /api/ {
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Max-Age "600" always;
if ($request_method = OPTIONS) {
return 204;
}
}The read-only public API. Nothing is echoed, so no map block and no Vary are warranted, and the OPTIONS branch holds return 204 alone.
Allow https://app.example.com · GET, POST, OPTIONS · Authorization and Content-Type · expose X-Request-Id · credentials on · Apache# CORS — Apache 2.4 (mod_headers, mod_setenvif, mod_rewrite).
# Place inside the <Directory>, <Location> or virtual host it applies to.
<IfModule mod_setenvif.c>
SetEnvIf Origin "^(https://app\.example\.com)$" CORS_ALLOW_ORIGIN=$1
</IfModule>
<IfModule mod_headers.c>
Header always set Access-Control-Allow-Origin "%{CORS_ALLOW_ORIGIN}e" env=CORS_ALLOW_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Expose-Headers "X-Request-Id"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Max-Age "600"
Header always set Vary "Origin"
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} =OPTIONS
RewriteRule ^ - [R=204,L]
</IfModule>The signed-in shape. SetEnvIf captures the matching origin into an environment variable and mod_headers writes it back, which is how one value is chosen per request. Checked with httpd -t against Apache 2.4.66.
Allow https://a.example, https://b.example, http://localhost:3000 · GET · the application answers OPTIONS itself · Express// CORS — Express. Register this before your routes and before any
// authentication middleware: a preflight OPTIONS carries no credentials.
const ALLOWED_ORIGINS = new Set([
"https://a.example",
"https://b.example",
"http://localhost:3000",
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (typeof origin === "string" && ALLOWED_ORIGINS.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
res.setHeader("Access-Control-Allow-Methods", "GET");
res.setHeader("Vary", "Origin");
next();
});Three origins, a development port among them. The short-circuit is off, so the middleware falls through to next() and your own OPTIONS route replies; turn it on and four lines answering 204 take the place of that fall-through.
Allow every origin · GET, POST · request headers * · expose * · server answers the preflight · Caddy# CORS — Caddy 2 (Caddyfile). Place inside the site block it applies to.
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, POST"
Access-Control-Allow-Headers "*"
Access-Control-Expose-Headers "*"
}
@cors_preflight method OPTIONS
respond @cors_preflight 204Caddyfile indentation is tabs, reproduced as emitted. Both lists are stars, which is legal and narrower than it looks: the star never covers Authorization, and means nothing at all once credentials are on.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
Vary: OriginAccess-Control-Allow-Origin: *
Any origin may read this response, and only when the request was sent without credentials — a browser refuses * the moment cookies or an Authorization header are attached.
Access-Control-Allow-Methods: GET, POST
A preflight asking for GET, POST is allowed. Any other method is refused, and this field is read only during the OPTIONS preflight.
Access-Control-Allow-Credentials: true
Cookies and Authorization headers are allowed on these requests, and the response is readable with them attached. This only works alongside an exact origin — never alongside *.
Vary: Origin
This response is cached per the listed request fields. With Origin in the list, a shared cache keeps the per-origin answers apart instead of serving the first caller's Access-Control-Allow-Origin to everybody.Explain mode, reading a deployment back. The status line stays out of the field list, each field gets its own sentence, and the clash between lines one and three is reported separately.
Edge case · allow every origin · credentials on · Authorization listed · Max-Age 600 · nginxH_CORS_WILDCARD_WITH_CREDENTIALS
A credentialed request cannot read a response whose Access-Control-Allow-Origin is *. Echo the request's Origin instead, and add Vary: Origin so caches keep the answers apart (WHATWG Fetch, CORS protocol).
E1_CORS_CREDENTIALS_WILDCARD
Nothing was emitted for nginx, Apache, Express or Caddy: no way of writing this pair in a configuration file makes it work. Switch the origin to an explicit allow-list, or turn credentials off.The one combination this page will not write out: the file would load cleanly and fail every credentialed call. No block is produced for any of the four servers, and the reason is printed instead.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
One add_header inside a block cancels the rest
nginx inherits these directives from the level above only while the inner level declares none of its own, and an if block counts as a level. Add one line to the emitted preflight branch and every allow field above it vanishes from that response.
Apache without always skips exactly the responses that matter
Header set touches the success table only; Header always set covers the error responses too. A preflight answering 401 without its allow fields is reported as a CORS failure rather than as the status it really was, so every emitted line carries always.
The allow-list is compared, never printed
The field carries exactly one value; a comma-separated list is not valid. Every list-mode block therefore matches the incoming Origin and writes back the entry that matched — an nginx map, an Apache SetEnvIf capture, a Set lookup, a Caddy matcher.
Max-Age is a ceiling request, not a guarantee
Anything above 86400 is emitted as 86400: browsers cap the preflight cache themselves, Chromium at 7200 seconds and Firefox at 86400, so a larger number only hides that. Emitting -1 switches the caching off deliberately.
Common errors
A credentialed request cannot read a response whose Access-Control-Allow-Origin is *. Echo the request's Origin instead, and add Vary: Origin so caches keep the answers apart (WHATWG Fetch, CORS protocol).- Raised the moment credentials and the wildcard meet, in either mode. Switching the selector to a list is the whole fix.
"https://*.example.com" uses "*" as a wildcard, and CORS has none. Access-Control-Allow-Origin is compared with the request's Origin byte for byte, so "*" only means anything as the whole value — the "Any origin" setting. To allow a set of subdomains, list each origin here, or look the Origin up per request in your application and echo the one that matched.- The classic ask, and the one CORS has no answer for.
"https://app.example.com/" ends in a slash. The Origin header a browser sends never does, and Access-Control-Allow-Origin is compared byte for byte, so this entry would never match. Write it as "https://app.example.com".- Almost always a value copied out of an address bar, which shows the slash. It is refused rather than trimmed silently: a trimmed value is a different allow-list than the one typed.
"HTTPS://App.Example.com:443" is not the form a browser sends. The Origin header carries the serialized origin — lowercase scheme and host, punycode for non-ASCII names, and no default port — and Access-Control-Allow-Origin is compared byte for byte. Write it as "https://app.example.com".- Capitals, an explicit :443 and an internationalised host name all land here, and the message carries the corrected spelling.
Vary: Origin is off while the response echoes one origin out of a list. A shared cache in front of this server will hand the first caller's Access-Control-Allow-Origin to the next one, and that response fails CORS for everybody else.- A warning, not a refusal: the block is still emitted. A CDN turns the omission into failures that come and go with cache hits.
Access-Control-Max-Age was emitted as 86400 rather than 604800. Browsers cap it themselves — Chromium at 7200 seconds (2 hours), Firefox at 86400 seconds (24 hours) — so a larger number buys nothing and hides what actually happens.- The clamp is reported rather than applied quietly, so the file and the form never disagree in silence.
FAQ
Where in my server configuration does the emitted block go?
Each block opens with a comment naming its home: an nginx location inside the matching server block, with any map above it in the http context; an Apache Directory, Location or virtual host; a Caddyfile site block; and for Express, a call registered ahead of the routes.
Why does the allow-list block echo one origin instead of listing them?
Because the field accepts a single value. The block compares what arrived against your list and writes back the entry that matched, which is also why Vary: Origin comes with it.
Do I still need the OPTIONS short-circuit if my framework handles OPTIONS?
No, and it is worth leaving off: two layers answering one preflight is a source of doubled headers. Switch it off and the block sets the fields, then steps aside.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Re-run the emitter
- ⌘/Ctrl + S
- Download the block
- ⌘/Ctrl + C
- Copy the block when nothing is selected
- ⌘/Ctrl + Shift + C
- Copy the block regardless of selection
- t
- Switch the theme
- ? or ⌘/Ctrl + /
- Open the shortcut help
- Esc
- Dismiss the help overlay