<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rasim Çarkçı</title>
    <description>The latest articles on DEV Community by Rasim Çarkçı (@rasimcarkci).</description>
    <link>https://dev.to/rasimcarkci</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2783735%2F9cd010b5-0a27-48a4-b42c-0451438eefdf.jpg</url>
      <title>DEV Community: Rasim Çarkçı</title>
      <link>https://dev.to/rasimcarkci</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rasimcarkci"/>
    <language>en</language>
    <item>
      <title>CORS Explained: Why Your API Requests Fail</title>
      <dc:creator>Rasim Çarkçı</dc:creator>
      <pubDate>Sun, 11 Oct 2026 14:26:55 +0000</pubDate>
      <link>https://dev.to/rasimcarkci/cors-explained-why-your-api-requests-fail-244o</link>
      <guid>https://dev.to/rasimcarkci/cors-explained-why-your-api-requests-fail-244o</guid>
      <description>&lt;p&gt;You fetch an API from JavaScript and get a cryptic &lt;em&gt;"blocked by CORS policy"&lt;/em&gt; error. Nothing in your code is wrong — it's the browser enforcing a security rule. Here is exactly what CORS is, why it exists, and how to fix it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://www.moreonlinetools.com/en/blog/what-is-cors-guide/" rel="noopener noreferrer"&gt;MoreOnlineTools Blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is CORS and Why Does It Exist?
&lt;/h2&gt;

&lt;p&gt;CORS stands for &lt;strong&gt;Cross-Origin Resource Sharing&lt;/strong&gt;. It is a browser security mechanism that controls how web pages load resources from a different &lt;em&gt;origin&lt;/em&gt; (domain, protocol, or port) than the page itself.&lt;/p&gt;

&lt;p&gt;The underlying rule CORS enforces is called the &lt;strong&gt;Same-Origin Policy (SOP)&lt;/strong&gt;. Browsers have applied SOP since the late 1990s: a script on &lt;code&gt;https://example.com&lt;/code&gt; cannot by default read responses from &lt;code&gt;https://api.otherdomain.com&lt;/code&gt;. Without this rule, any malicious website could silently fetch your bank page, read the response, and steal your account data using your already-logged-in session cookies.&lt;/p&gt;

&lt;p&gt;CORS does not block the &lt;em&gt;request&lt;/em&gt; itself — your browser still sends it. CORS blocks &lt;strong&gt;JavaScript from reading the response&lt;/strong&gt; when the server has not explicitly said "this origin is allowed."&lt;/p&gt;

&lt;h2&gt;
  
  
  What Counts as a Different Origin?
&lt;/h2&gt;

&lt;p&gt;Two URLs share the same origin only if all three of these match exactly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; &lt;code&gt;http&lt;/code&gt; vs &lt;code&gt;https&lt;/code&gt; are different origins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain:&lt;/strong&gt; &lt;code&gt;example.com&lt;/code&gt; vs &lt;code&gt;api.example.com&lt;/code&gt; are different origins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; &lt;code&gt;localhost:3000&lt;/code&gt; vs &lt;code&gt;localhost:8080&lt;/code&gt; are different origins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;https://app.example.com&lt;/code&gt; and &lt;code&gt;https://api.example.com&lt;/code&gt; are &lt;em&gt;cross-origin&lt;/em&gt;, even though they share the same root domain. Subdomains always count as different origins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple Requests vs Preflight Requests
&lt;/h2&gt;

&lt;p&gt;Not all cross-origin requests trigger the same CORS flow. Browsers split them into two categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple Requests
&lt;/h3&gt;

&lt;p&gt;A request is "simple" if it meets all of these conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Method is &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, or &lt;code&gt;HEAD&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Only uses safe headers: &lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;Accept-Language&lt;/code&gt;, &lt;code&gt;Content-Language&lt;/code&gt;, or &lt;code&gt;Content-Type&lt;/code&gt; limited to &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;, &lt;code&gt;multipart/form-data&lt;/code&gt;, or &lt;code&gt;text/plain&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No event listeners on &lt;code&gt;XMLHttpRequestUpload&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For simple requests, the browser sends the request directly and checks the response for &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;. If the header is missing or the origin does not match, the browser blocks JavaScript from reading the response and logs a CORS error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preflight Requests
&lt;/h3&gt;

&lt;p&gt;Any request that is not "simple" — for example, a &lt;code&gt;PUT&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;, a request with &lt;code&gt;Authorization&lt;/code&gt; or &lt;code&gt;Content-Type: application/json&lt;/code&gt;, or a custom header — triggers a &lt;strong&gt;preflight&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A preflight is an automatic &lt;code&gt;OPTIONS&lt;/code&gt; request the browser sends &lt;em&gt;before&lt;/em&gt; the actual request. The browser asks the server: "I want to send a POST with JSON and an Authorization header from this origin. Is that allowed?" The server must respond with the appropriate CORS headers. Only then does the browser proceed with the actual request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;OPTIONS&lt;/span&gt; &lt;span class="nn"&gt;/api/data&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;
&lt;span class="na"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://app.mysite.com&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Request-Method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Request-Headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Content-Type, Authorization&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A correct server response to a preflight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;204&lt;/span&gt; &lt;span class="ne"&gt;No Content&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://app.mysite.com&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET, POST, PUT, DELETE, OPTIONS&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Allow-Headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Content-Type, Authorization&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Max-Age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;86400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Access-Control-Max-Age&lt;/code&gt; tells the browser how many seconds to cache the preflight result — avoiding a round-trip on every subsequent request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Key CORS Response Headers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which origins may read the response&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;https://app.mysite.com&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which HTTP methods are permitted&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET, POST, DELETE, OPTIONS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which request headers are permitted&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Content-Type, Authorization&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Access-Control-Allow-Credentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether cookies/auth may be included&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which response headers JS can read&lt;/td&gt;
&lt;td&gt;&lt;code&gt;X-Request-Id, X-RateLimit-Remaining&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Access-Control-Max-Age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seconds to cache the preflight result&lt;/td&gt;
&lt;td&gt;&lt;code&gt;86400&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Wildcard Trap: Why &lt;code&gt;*&lt;/code&gt; Is Not Always Enough
&lt;/h2&gt;

&lt;p&gt;Setting &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; seems like the easiest fix — and for public read-only APIs it usually works fine. But there is one critical limitation: &lt;strong&gt;you cannot use &lt;code&gt;*&lt;/code&gt; with credentials&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your request includes cookies, HTTP authentication, or a TLS client certificate (i.e. you set &lt;code&gt;credentials: "include"&lt;/code&gt; in &lt;code&gt;fetch()&lt;/code&gt;), the browser requires an explicit origin, not a wildcard. The server must also explicitly set &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt;. Using &lt;code&gt;*&lt;/code&gt; in this scenario results in a CORS error even though the header is present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;// ❌ This combination is ALWAYS rejected by the browser:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

// ✅ Correct for credentialed requests:
Access-Control-Allow-Origin: https://app.mysite.com
Access-Control-Allow-Credentials: true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Fix CORS: Backend Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PHP (Native)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nv"&gt;$allowed_origins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'https://app.mysite.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'https://staging.mysite.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_ORIGIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowed_origins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Access-Control-Allow-Origin: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$origin&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Credentials: true'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Allow-Headers: Content-Type, Authorization'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access-Control-Max-Age: 86400'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Handle preflight&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REQUEST_METHOD'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'OPTIONS'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Node.js / Express
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://app.mysite.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://staging.mysite.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPTIONS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;allowedHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nginx
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"https://app.mysite.com"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Methods&lt;/span&gt; &lt;span class="s"&gt;"GET,&lt;/span&gt; &lt;span class="s"&gt;POST,&lt;/span&gt; &lt;span class="s"&gt;PUT,&lt;/span&gt; &lt;span class="s"&gt;DELETE,&lt;/span&gt; &lt;span class="s"&gt;OPTIONS"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Headers&lt;/span&gt; &lt;span class="s"&gt;"Content-Type,&lt;/span&gt; &lt;span class="s"&gt;Authorization"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Credentials&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;OPTIONS)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Max-Age&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Apache (.htaccess)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Access-Control-Allow-Origin "https://app.mysite.com"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Access-Control-Allow-Headers "Content-Type, Authorization"
&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Access-Control-Allow-Credentials "true"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multiple Allowed Origins (Dynamic Validation)
&lt;/h2&gt;

&lt;p&gt;You cannot list multiple origins in a single &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header — it only accepts one value or &lt;code&gt;*&lt;/code&gt;. The correct pattern is to validate the incoming &lt;code&gt;Origin&lt;/code&gt; header against a server-side allowlist and echo it back if it matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Generic pattern (any language)
IF request.origin IN allowlist:
    response.header("Access-Control-Allow-Origin", request.origin)
    response.header("Vary", "Origin")  // Important for CDN caching
ELSE:
    response.header("Access-Control-Allow-Origin", "null")  // deny
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;Vary: Origin&lt;/code&gt; header — without it, a CDN or proxy might cache a response intended for one origin and serve it to a different one.&lt;/p&gt;

&lt;h2&gt;
  
  
  CORS Is Not a Security Feature for the Server
&lt;/h2&gt;

&lt;p&gt;This is the most common misconception about CORS. &lt;strong&gt;CORS does not protect your API from unauthorized access.&lt;/strong&gt; It only controls what browsers do with responses. A &lt;code&gt;curl&lt;/code&gt; request, a Postman request, or a server-to-server request completely ignores CORS — browsers are the only enforcement point.&lt;/p&gt;

&lt;p&gt;If you need to restrict who can call your API, use proper authentication (JWT, API keys, OAuth) and authorization checks on the server side. CORS is for protecting users' browsers from malicious websites reading cross-site responses — not for protecting your API from unauthorized callers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common CORS Errors and What They Mean
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error message&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No 'Access-Control-Allow-Origin' header is present&lt;/td&gt;
&lt;td&gt;Server never sends the header&lt;/td&gt;
&lt;td&gt;Add the header to your server/backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The value of 'Access-Control-Allow-Origin' is not equal to the supplied origin&lt;/td&gt;
&lt;td&gt;Header is present but origin does not match&lt;/td&gt;
&lt;td&gt;Use dynamic origin echo or correct whitelist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential flag is true, but the 'Access-Control-Allow-Credentials' header is not 'true'&lt;/td&gt;
&lt;td&gt;Using credentials without the header&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt; and remove wildcard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request header field Authorization is not allowed by Access-Control-Allow-Headers&lt;/td&gt;
&lt;td&gt;The preflight rejected the Authorization header&lt;/td&gt;
&lt;td&gt;Add Authorization to &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method PUT is not allowed by Access-Control-Allow-Methods&lt;/td&gt;
&lt;td&gt;Method missing from the preflight response&lt;/td&gt;
&lt;td&gt;Add the method to &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  CORS in Local Development
&lt;/h2&gt;

&lt;p&gt;During development you often hit CORS errors because your frontend runs on &lt;code&gt;localhost:3000&lt;/code&gt; (Vite, Create React App, etc.) while your backend runs on &lt;code&gt;localhost:8000&lt;/code&gt;. Different ports = different origins.&lt;/p&gt;

&lt;p&gt;The cleanest solution is to add &lt;code&gt;http://localhost:3000&lt;/code&gt; to your server's CORS allowlist in development mode. An alternative is to use your bundler's proxy feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;changeOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a dev proxy, the browser thinks all requests go to &lt;code&gt;localhost:3000&lt;/code&gt; — no cross-origin, no CORS. This is the approach used by most modern frontend frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CORS is enforced by &lt;strong&gt;browsers only&lt;/strong&gt; — servers and tools like curl are unaffected&lt;/li&gt;
&lt;li&gt;It restricts reading responses, not sending requests&lt;/li&gt;
&lt;li&gt;Simple requests go through directly; non-simple requests trigger an &lt;code&gt;OPTIONS&lt;/code&gt; preflight&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; cannot be used together with credentials&lt;/li&gt;
&lt;li&gt;Always echo the specific origin dynamically and include &lt;code&gt;Vary: Origin&lt;/code&gt; when using an allowlist&lt;/li&gt;
&lt;li&gt;CORS does not replace authentication — it is a browser-level protection for users, not an API access control mechanism&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Want to check whether your server sends correct CORS and security headers? Try our free &lt;a href="https://www.moreonlinetools.com/en/tools/http-security-headers-analyzer/" rel="noopener noreferrer"&gt;HTTP Security Headers Analyzer&lt;/a&gt; — paste any URL and get an instant breakdown of every header, including &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;. Runs entirely in your browser.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
