Messages like Access to fetch has been blocked by CORS policy almost certainly appear when the frontend and backend are running at different addresses. For example, a JavaScript application is at http://localhost:3000, while the PHP API is at http://localhost:8000. The request seems simple, but the browser rejects it before the API result can be read by JavaScript.
The common reaction is to look for ways to "disable CORS." This approach is misguided. CORS, or Cross-Origin Resource Sharing, is not a flaw in the application, but rather an HTTP mechanism that allows the server to declare which origins are permitted to read its response. This mechanism works alongside the same-origin policy, a browser rule that restricts access between sites. MDN explains that the origin is determined by a combination of protocol, hostname, and port.
Why can the frontend be rejected by the browser?
The following two addresses appear to come from the same computer, but are still considered different:
http://localhost:3000http://localhost:8000
The hostname is the same, but the ports are different. Similarly, http://example.com and https://example.com: the protocols are different, so the origins are different.
The browser can still send some types of cross-origin requests, such as through an HTML form. However, the browser restricts JavaScript's ability to read responses from other origins. Without this restriction, a malicious site could attempt to read data from another application that the user has open.
This is where CORS comes into play. The server sends specific headers to state, "this origin is allowed to read my response." If the header is absent or its value is incorrect, the browser blocks access from JavaScript code. The server may have actually sent a response, but that response is not provided to the frontend application.
Understanding preflight without scary terms
For certain requests, the browser sends a preliminary request called preflight. This request uses the OPTIONS method and asks the server whether the methods and headers to be used are allowed.
For example, the frontend sends the following request:
fetch('https://api.example.com/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ product_id: 12 })
});Since it uses the POST method with JSON and authorization headers, the browser may send a preflight first. The API server needs to respond with the appropriate headers, for example:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, AuthorizationIf the server does not handle OPTIONS, returns an error status, or does not allow the requested headers, the main request will not proceed. MDN CORS documentation explains that preflight is used by the browser to check methods and headers before requests that could potentially change data.
Do not use Access-Control-Allow-Origin: *
The wildcard * is indeed practical for public APIs that do not use user credentials. However, the wildcard is not a safe option for all cases.
For example, a dashboard API returns order data based on login cookies. If the server allows all origins indiscriminately, other sites could potentially try to access that endpoint from the victim's browser. The risk becomes more serious if the server also allows credentials.
For applications that use cookies or credential-based authentication, use a specific origin:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueDo not combine Access-Control-Allow-Credentials: true with Access-Control-Allow-Origin: *. The browser does not consider this combination valid for credentialed requests. For public APIs without personal data, a wildcard can be used, but ensure that the endpoint is indeed accessible to anyone.
OWASP also recommends that CORS configurations be as specific as possible and not used when cross-origin communication is not needed. CORS is not a substitute for authentication and authorization. The server must still check tokens, access rights, and data ownership at every endpoint.
Simple configuration example in PHP
For a simple PHP API, the configuration can start like this:
<?php
$allowedOrigin = 'https://app.example.com';
if (isset($_SERVER['HTTP_ORIGIN']) &&
$_SERVER['HTTP_ORIGIN'] === $allowedOrigin) {
header("Access-Control-Allow-Origin: $allowedOrigin");
header('Access-Control-Allow-Credentials: true');
header('Vary: Origin');
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}This example is not a universal configuration. In real projects, the list of origins should be stored in the application configuration, not written repeatedly in many files. If there are multiple legitimate frontends, match the Origin value with a clear allowlist. Do not simply reflect the Origin value from the request without validation.
The Vary: Origin header is also important when the server selects responses based on the origin. This header informs the cache that the response may differ for different Origin values.
How to read CORS errors more calmly
When CORS fails, do not just look at the error line in the frontend. Open the Network tab in the browser's DevTools, and check the following:
- Does the
OPTIONSrequest appear before the main request? - What is the status code of the preflight response?
- Does
Access-Control-Allow-Originmatch the frontend origin, including protocol and port? - Does the method in
Access-Control-Allow-Methodsinclude the method being used? - Are all requested headers listed in
Access-Control-Allow-Headers? - Is the request using cookies or credentials, but the server has not enabled them?
If the API is behind Nginx, a reverse proxy, CDN, or gateway, also check those layers. It is possible that PHP has already sent the correct headers, but the proxy removes them or does not forward the OPTIONS request.
What does this mean for us?
CORS should be treated as part of API design, not a patch after the frontend is complete. Determine from the start which frontends are allowed to access the API, which endpoints are public, and whether authentication uses cookies or tokens.
What can be done now is to create an origin allowlist for development and production environments, handle OPTIONS requests explicitly, test credentialed requests, and ensure that authentication is still checked on the server side. This way, the CORS solution not only eliminates errors but also keeps the API functioning without opening access wider than necessary.
Sources & further reading
- Cross-Origin Resource Sharing (CORS) - MDN Web Docs
- Same-origin policy - MDN Web Docs
- Cross-Origin Resource Sharing (CORS) configuration - MDN Web Docs
- REST Security Cheat Sheet - OWASP
โ Rio Yotto @rioyotto
