title: "A CORS preflight nem HTTP cache: miért nem tűnik el az OPTIONS kérés?"
slug: cors-preflight-kulon-cache-options
excerpt: "Az Access-Control-Max-Age hasznos, de a CORS preflightot nem a szokásos HTTP cache kezeli. Ez megmagyarázza, miért maradnak meg az OPTIONS kérések Vary és CDN mellett is."
—
Ha egy külön frontend és API között minden POST előtt felbukkan egy OPTIONS, az első reakció gyakran az, hogy „ezt majd elcache-eli a CDN”. Aztán nem cache-eli. Vagy legalábbis nem ott és nem úgy, ahogy várnánk. A CORS preflight a böngésző saját engedélykérő köre: az API-nak még a tényleges kérés előtt ki kell mondania, hogy az adott origin, metódus és fejléckészlet rendben van.

Ez nem minden cross-origin fetch előtt történik. A CORS-safelisted kérés például GET, HEAD, vagy megfelelő korlátokkal POST lehet. De egy tipikus JSON API-hívás, Content-Type: application/json-nal és Authorization fejléccel már kilóg ebből a körből. A böngésző előbb nagyjából ezt küldi:
OPTIONS /orders HTTP/1.1
Origin: https://app.example.test
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type, x-request-id
Az API válasza nem a rendelést hozza létre. Csak engedélyt ad rá, hogy a böngésző ezután megpróbálhassa:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.test
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: authorization, content-type, x-request-id
Access-Control-Max-Age: 600
Vary: Origin
Az OPTIONS-válasz nem a „rendes” cache-be kerül
A meglepő rész itt következik. A preflight válaszát a böngésző külön, CORS-preflight cache-ben tartja; nem a normál HTTP cache-ben. Emiatt a Cache-Control, az ETag, a CDN cache szabálya vagy az OPTIONS válaszra tett hosszú s-maxage nem a böngésző preflight-döntését vezérli. Az Access-Control-Max-Age igen.
Na, ezt nem tudtam: a Fetch szabvány cache-bejegyzése nem pusztán hostonként létezik. A hálózati partíció, a kérő origin, a pontos URL és a credentials mód is számít; külön metódus- és fejlécengedélyeket tárol. Emiatt egy /orders/42 és egy /orders/43 preflightja nem feltétlenül osztozik ugyanazon az engedélyen, és egy beágyazott widgetnek sem kell ugyanazt a cache-t megkapnia, mint a saját alkalmazásodnak.
Ez szándékos. A CORS nem sebességfunkció, hanem böngészőoldali hozzáférés-ellenőrzés. Ha túl tág kulccsal működne, egy másik originnek vagy más kérésnek maradhatna ott egy olyan engedély, amihez semmi köze. A browser inkább kérdez még egyszer; az API-szerver kibírja.
Mire jó akkor a Vary: Origin?
A Vary: Origin továbbra is kell, ha a szerver az Origin alapján más Access-Control-Allow-Origin értéket ad. Ez a normál HTTP cache-eknek — reverse proxy, CDN, böngésző HTTP cache — jelzi, hogy az egyik originnek készített válasz nem adható oda vakon a másiknak. A Fetch szabvány CORS és HTTP cache fejezete külön ki is emeli ezt a helyzetet.
Csakhogy a Vary nem a preflight cache kulcsának konfigurációja. Nem rövidíti meg az OPTIONS-t, nem tesz két URL-t azonosnak, és nem pótolja az Access-Control-Max-Age fejlécet. Ez két cache két külön problémával. Az egyik a HTTP-reprezentációt, a másik azt tárolja, milyen cross-origin műveletet szabad megkísérelni. A hasonló nevű fejlécmezők itt csak arra jók, hogy egy hibajegyben fél délutánt elvigyenek.
Access-Control-Max-Age: legyen értelmes, ne végtelen
Az Access-Control-Max-Age: 600 józan kiindulópont egy stabil API-nál: tíz percig nincs új engedélykérő kör azonos feltételek mellett. A fejléc dokumentációja szerint az alapértelmezés öt másodperc, és a böngészők saját felső korlátot is alkalmaznak. Vagyis a 31536000 nem egy év megbízható ígérete, csak egy magabiztos szám a konfigurációban.
Nem érdemes a legnagyobb értéket reflexből beírni. Ha gyakran változik az engedélyezett fejléc vagy metódus, a kliens a régi engedély miatt később veszi észre a módosítást. Ez önmagában nem megkerülhető jogosultság — a tényleges kérésre továbbra is a szerver dönt —, de üzemeltetési szempontból kellemetlen, amikor egy CORS-változtatás csak néhány böngészőben „nem ment át”. A szabvány azt is megengedi, hogy a böngésző a lejárat előtt kidobja a bejegyzést, tehát ez optimalizálás, nem SLA.
Egy szigorú Node példa
Az alábbi szerver nem tükrözi vissza vaktában az Origin-t, és a kért metódust, illetve fejléceket is ellenőrzi. Ez a rész fontosabb, mint az, hogy az OPTIONS 204-et vagy 200-at küld: a CORS biztonsági útmutató credentialös helyzetben különösen óv az origin reflexes visszaadásától.
import { createServer } from 'node:http';
const allowedOrigins = new Set(['https://app.example.test']);
const allowedMethods = new Set(['GET', 'POST', 'DELETE']);
const allowedHeaders = new Set(['authorization', 'content-type', 'x-request-id']);
function handleCors(req, res) {
const origin = req.headers.origin;
if (!allowedOrigins.has(origin)) return false;
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
const requestedMethod = req.headers['access-control-request-method'];
if (req.method !== 'OPTIONS' || !requestedMethod) return false;
const requestedHeaders = (req.headers['access-control-request-headers'] ?? '')
.split(',').map((header) => header.trim().toLowerCase()).filter(Boolean);
if (!allowedMethods.has(requestedMethod) ||
requestedHeaders.some((header) => !allowedHeaders.has(header))) {
res.writeHead(403).end();
return true;
}
res.writeHead(204, {
'Access-Control-Allow-Methods': [...allowedMethods].join(', '),
'Access-Control-Allow-Headers': [...allowedHeaders].join(', '),
'Access-Control-Max-Age': '600',
}).end();
return true;
}
createServer((req, res) => {
if (handleCors(req, res)) return;
if (req.url === '/orders' && req.method === 'POST') {
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end('{"ok":true}');
return;
}
res.writeHead(404).end();
}).listen(Number(process.env.PORT ?? 3000));
Élesben ezt tipikusan framework middleware, API gateway vagy ingress intézi, de a logika ugyanaz. Ha session cookie kell, külön kérés a credentials: 'include', az explicit origin és az Access-Control-Allow-Credentials: true; wildcarddal ezt nem lehet összebékíteni. A preflight maga ettől még nem visz cookie-t, csak a tényleges kérés lehet credentialös.
Mit mérj, mit javíts?
Először DevTools Network nézetben szűrj OPTIONS-ra. Ha egy oldalbetöltés alatt rengeteg különböző, dinamikus URL-re hívsz JSON API-t egyedi fejlécekkel, a preflight cache eleve kevés ismétlődést talál. Itt sokszor többet ér az API útvonalainak és klienshívásainak rendbetétele, mint a max-age tekergetése.
Másodszor csak azokat a request headereket küldd, amelyeknek van értelmük. Az „minden kéréshez kitalálunk egy új X-* fejlécnevet” megfigyelési vagy kliensmeta-szokás könnyen új preflight-feltétellé válik. Nem azért kell eltüntetni, hogy trükkösen megkerüld a CORS-t; ez biztonsági határ. Hanem azért, mert a cross-origin API szerződése legyen szűk és szándékos.
Én productionben beállítanék mérsékelt Access-Control-Max-Age-t, explicit origin-, metódus- és fejlérlistát, valamint Vary: Origin-t minden dinamikus originválasznál. Nem építenék viszont CDN-es „OPTIONS cache megoldásra” úgy, mintha attól a böngésző engedélykérése eltűnne. A CDN a szerverterhelést csökkentheti; a böngésző külön cache-e viszont saját szabályok szerint dolgozik. Ugyanaz a szó, cache, csak épp teljesen más fiókban van.