Anyone who has wired up a browser-side call to an API on a different domain has probably hit the red has been blocked by CORS policy message in the console — even though the server responded just fine. The browser is the one refusing to hand the response over. This article walks through the same-origin policy that causes this, and what CORS (Cross-Origin Resource Sharing) actually does to relax it safely.
What “origin” means
Note: an origin is the combination of a URL’s scheme (
https://), hostname (wpmm.jp), and port. The path (/blog/, etc.) doesn’t count.https://wpmm.jpandhttps://en.wpmm.jpare different origins because the hostname differs;https://wpmm.jpandhttp://wpmm.jpare different origins too, because the scheme differs.
Browsers enforce a baseline rule called the same-origin policy: JavaScript loaded from one origin generally cannot read the response of a request made to a different origin. This restriction lives in the browser itself — it has nothing to do with server configuration.
Why the restriction exists at all
The reasoning becomes obvious once you imagine a world without it. If the same-origin policy didn’t exist, simply opening a malicious page in your browser would let its JavaScript quietly send a request to your bank’s site in the background, using the authentication cookies already stored in your browser, and read the response — your account balance, for instance. The browser doesn’t stop the request from being sent; what it stops is letting the page’s JavaScript read the response that comes back from a different origin. That single restriction closes off a large class of data-theft attacks.
CORS is how a server safely loosens that restriction
Plenty of legitimate use cases need to call an API on a different origin, though. CORS is the mechanism a server uses to explicitly declare “reads from this origin are allowed.” When a server includes a header like Access-Control-Allow-Origin: https://example.com in its response, the browser permits JavaScript running on that specific origin to read the response — and only that origin.
The key point is that the decision is made by the browser, not the server. The server is only offering a header; whether the response actually gets handed to the calling JavaScript is enforced client-side. A tool like curl or Postman, which isn’t a browser, always sees the full response regardless of any CORS header — the restriction only kicks in for JavaScript running inside a browser.
Preflight requests
For a “simple” GET request, the browser sends the real request first and then decides whether to expose the response based on the headers it gets back. But for a request that isn’t considered simple — a POST with Content-Type: application/json, for example — the browser sends an OPTIONS request first, called a preflight, before it ever sends the real one. Only once the server responds to that preflight with headers like Access-Control-Allow-Methods and Access-Control-Allow-Headers does the browser go ahead and send the actual request.
Wildcard * vs. a specific origin
Access-Control-Allow-Origin can either allow any origin via the wildcard *, or allow a single specific origin by name. The deciding factor is whether the API’s response depends on credentials such as cookies or an Authorization header.
- A public, stateless endpoint that returns the same result no matter who calls it can safely use
*. - An endpoint whose response is tied to a cookie-based session, and therefore contains something personal to the caller, can’t use
*— browsers won’t allowAccess-Control-Allow-Credentials: trueto be paired with a wildcard origin. Instead, the server has to maintain an allowlist and match it against the incomingOriginheader on each request, echoing back only the origins it trusts.
How our own backend splits this
Two API endpoints behind our landing page happen to illustrate this split directly.
The chatbot API (server/wpmm-web/api/chat.php) checks the incoming Origin header against an explicit allowlist and only echoes it back on a match:
$allowed_origins = [
'https://wpmm.jp',
'https://www.wpmm.jp',
'https://en.wpmm.jp',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed_origins, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
}
Because this endpoint tracks rate limits and conversation logs per session, letting any arbitrary site embed it would open the door to abuse. Restricting the allowed origins to our own landing pages keeps the set of places it can be called from under our control. The Vary: Origin header alongside it is deliberate too — it tells any caching layer (a CDN, or the browser’s own cache) that the response body depends on the Origin header’s value, so a response approved for one origin never gets served from cache to a request from a different one.
The checkout-session API (server/wpmm-web/api/checkout.php), on the other hand, returns Access-Control-Allow-Origin: *. It only creates a Stripe checkout session and hands back a redirect URL — nothing in that response is tied to the caller’s cookies or identity — so there’s little reason to restrict which origin can call it.
There’s a third pattern worth mentioning too: the “latest posts” section on our landing page (includes/blog_latest.php) doesn’t call the blog’s REST API from browser JavaScript at all. Instead, the PHP running on the server fetches the REST API itself and renders the result as plain HTML. A server-to-server request like that never goes through a browser, so the same-origin policy and CORS simply don’t apply to it. “Restrict which browser origins can call this directly” and “don’t let the browser make the call in the first place” are two different answers to the same underlying problem.
Takeaway
CORS is how a server safely relaxes the browser’s default same-origin restriction by explicitly declaring which origins it trusts. The browser is the one enforcing the rule; the server is only stating a preference via a header. Whether to use a wildcard or an allowlist comes down to whether the response is tied to credentials. And it’s worth remembering that CORS is a browser-specific concept in the first place — routing a call through the server instead of directly from client-side JavaScript sidesteps the whole question.