The message blocked by CORS policy almost certainly appears when the frontend and backend are developed at different addresses. For example, a JavaScript application runs at http://localhost:3000, while the PHP API is at http://localhost:8000. The server may be running, and the endpoint can be accessed with Postman, but the browser still blocks its response.
Understanding CORS is important because quick fixes—such as allowing all origins or disabling browser checks—can make the application run smoothly during development but leave security vulnerabilities when deployed publicly.
What is CORS, really?
CORS, or Cross-Origin Resource Sharing, is a mechanism that regulates whether a web page from one origin can access resources from another origin. An origin consists of a combination of protocol, host, and port.
This means that the following addresses are considered different even though they are running on the same computer:
http://localhost:3000http://localhost:8000https://localhost:3000
Just a difference in port and protocol is enough to change the origin. Browsers enforce these rules to protect users. Without such restrictions, a malicious site could potentially send requests to other services that the user has open and read their data.
Thus, CORS is not a security system that only exists on the server side. It is a rule primarily enforced by the browser when JavaScript attempts to read cross-origin responses.
Why can an API succeed in Postman but fail in the browser?
Postman, cURL, and backend tools typically do not enforce the same browser security policies. When a request succeeds in Postman, it only indicates that the server received the request. It does not necessarily mean the browser is allowed to read its response.
For example, an API endpoint might return a status of 200 OK, but it does not include the header:
Access-Control-Allow-Origin: http://localhost:3000The browser receives the response from the network and then blocks JavaScript access to the contents of that response. From a developer's perspective, the result appears as if the request failed, even though the server may have already processed it.
Distinguish between simple requests and preflight
Not all cross-origin requests are treated the same. Some simple requests can be sent directly. However, requests with certain methods or headers will be preceded by a check called preflight.
Preflight is an OPTIONS request sent by the browser to ask the server: is this origin allowed to use the requested methods and headers?
For example, the frontend sends a request:
fetch('https://api.example.com/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ product_id: 10 })
});Because it uses the POST method, special headers, and JSON format, the browser may send an OPTIONS request first. The server must respond correctly, including permissions for the origin, methods, and headers.
Headers that are usually required
CORS implementation varies depending on the framework, but the basic concept is the same. The server needs to explain access rules through HTTP headers.
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, AuthorizationIf the application uses cookies or browser sessions, the following is usually also required:
Access-Control-Allow-Credentials: trueHowever, when Access-Control-Allow-Credentials: true is used, the value of Access-Control-Allow-Origin cannot be a wildcard (*). The server must specify the origin explicitly.
Example handling in PHP
For a simple PHP-based API, CORS rules can be placed before any output is sent:
<?php
$allowedOrigin = 'http://localhost:3000';
if (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['HTTP_ORIGIN'] === $allowedOrigin) {
header("Access-Control-Allow-Origin: $allowedOrigin");
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}This example only allows one origin. In a real application, the list of origins could be stored in a configuration file and compared with the value of the Origin header. Avoid accepting the origin value from the request and directly reflecting it without validation.
Common mistakes
Using wildcard for all conditions
Configurations like Access-Control-Allow-Origin: * are indeed practical for public APIs that do not use credentials. However, this configuration is not suitable for endpoints that rely on cookies, sessions, or user data.
Only handling GET and POST
Developers often add CORS headers to the main endpoint but forget to handle OPTIONS requests. As a result, the actual request never runs because the preflight has already failed.
Adding headers in the wrong place
Headers must be sent before the server outputs HTML, spaces, or error messages. In PHP, even a small output before calling header() can cause the headers to no longer be changeable.
Thinking CORS can fix authentication
CORS only regulates whether the browser can read cross-origin responses. It is not a substitute for authentication, authorization, input validation, CSRF protection, or access restrictions on the server.
More targeted debugging methods
- Check the frontend origin. Note the protocol, domain, and port that the browser is actually using.
- Open the Network tab. Look for the API request and see if there is an
OPTIONSrequest before the main request. - Check the response headers. Ensure
Access-Control-Allow-Originmatches the frontend origin. - Check methods and headers. If the frontend uses
AuthorizationorContent-Type: application/json, the server must allow it. - Test without concluding from Postman. Postman is useful for checking APIs, but it cannot ensure browser behavior.
What does this mean for us?
CORS should be treated as part of API design, not a patch after an error occurs. Determine from the start which frontends are allowed to access the API, whether authentication uses cookies or tokens, and which methods are needed.
For development, explicitly allow local origins. For production, use a list of truly trusted domains. Do not make * a permanent solution just because it eliminates errors.
If an API is indeed intended for public use, loose CORS can make sense—but endpoints still need to have validation, rate limiting, authentication if necessary, and protection against abuse. CORS solves the issue of read permissions by the browser; it does not solve all web application security issues.
– Rio Yotto @rioyotto
