Web APIs
CORS
Understand browser security restrictions and learn how to configure Cross-Origin Resource Sharing for secure API access.
Netflix serves video content from dozens of different domains. Their web player runs on netflix.com but fetches data from api-global.netflix.com, content from assets.nflxext.com, and analytics from ichnaea-web.netflix.com. Without proper CORS configuration, your browser would block every single request and Netflix would be a blank screen.CORS stands for Cross-Origin Resource Sharing, and it solves one of the web's most fundamental security challenges. When JavaScript code running on one domain tries to make HTTP requests to another domain, browsers step in with a security policy called the Same-Origin Policy.
This policy exists because without it, malicious websites could silently steal your data from other sites you're logged into. Imagine visiting evil-site.com, which then secretly makes requests to your-bank.com using your logged-in session. CORS prevents this nightmare scenario while still allowing legitimate cross-domain API communication.
But modern web applications need cross-domain requests all the time. Your frontend might run on localhost:3000 during development while hitting an API on localhost:8000. In production, your app might be on app.company.com while your API lives on api.company.com. CORS provides the controlled mechanism that makes this possible.
What Counts as a Different Origin
Origins have three components that must all match for requests to be considered "same-origin." Change any one piece and you've crossed into CORS territory.The protocol matters. https://api.example.com and http://api.example.com are different origins. The domain matters. api.example.com and cdn.example.com are different origins. The port matters. localhost:3000 and localhost:8000 are different origins.
This strictness catches developers off guard. You might think subdomains would be treated as the same origin, but they're not. Moving from HTTP to HTTPS during development breaks things. Even implied ports count — port 80 for HTTP and 443 for HTTPS are still different origins if you specify them explicitly.
Same Origin
https://app.example.com
https://app.example.com/api
https://app.example.com:443
Different Origin
https://api.example.com
http://app.example.com
https://app.example.com:8080
Simple vs Preflighted Requests
Not all cross-origin requests trigger the same CORS behavior. Browsers classify requests into two categories based on their complexity and potential for causing server side effects.Simple requests get sent immediately with CORS headers checked afterward. These include GET, HEAD, and POST requests with basic content types like application/x-www-form-urlencoded or text/plain. The browser sends the request and then validates the response headers to decide whether JavaScript should see the result.
Preflighted requests trigger an OPTIONS request first. This happens with PUT, DELETE, PATCH methods, custom headers like Authorization, or JSON content types. The browser asks the server "would you allow this request?" before sending the actual data.
The preflight system exists because servers built before CORS might not expect complex cross-origin requests. A legacy API that never anticipated DELETE requests from web browsers shouldn't suddenly receive them without warning. The preflight gives servers a chance to explicitly opt-in to these interactions.
Simple Requests
GET, HEAD, POST methods
Basic content types only
No custom headers
Preflighted Requests
PUT, DELETE, PATCH methods
JSON content type
Authorization headers
Essential CORS Headers
CORS communication happens entirely through HTTP headers. The server uses these headers to tell browsers exactly what cross-origin requests it will accept and under what conditions.| Header | Purpose | APIForge Usage |
|---|---|---|
| Access-Control-Allow-Origin | Specifies which origins can access the resource | Allow dashboard.apiforge.com to call api.apiforge.com |
| Access-Control-Allow-Methods | Lists HTTP methods permitted in cross-origin requests | Enable GET, POST, PUT, DELETE for API endpoints |
| Access-Control-Allow-Headers | Defines which headers the client can send | Accept Authorization and Content-Type headers |
| Access-Control-Allow-Credentials | Controls whether cookies/auth can be included | Allow authenticated requests from frontend apps |
| Access-Control-Max-Age | Caches preflight responses to reduce requests | Cache preflight results for 24 hours |
Access-Control-Allow-Origin is the most critical header. Set it to * and you allow requests from any domain — convenient for development but dangerous in production. Set it to a specific domain and you lock down access to just that origin. You can only specify one origin per response, not a list.
Access-Control-Allow-Credentials deserves special attention. When set to true, it allows requests to include cookies, authorization headers, and client certificates. But this comes with a restriction — you cannot use a wildcard origin when credentials are allowed. The browser enforces this security rule strictly.
Implementing CORS in Practice
The APIForge Backend team needs to configure CORS for their new developer dashboard. The React frontend runs on dashboard.apiforge.com and needs to make authenticated API calls to api.apiforge.com for user data, project metrics, and billing information.// APIForge API server CORS configuration
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: ['https://dashboard.apiforge.com', 'https://app.apiforge.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));
app.get('/api/user/profile', (req, res) => {
res.json({ id: 1, name: 'Sarah Chen', role: 'Backend Engineer' });
});What just happened?
The Express server configured CORS to accept requests from two specific APIForge domains. When the dashboard makes requests, these headers tell the browser the cross-origin call is permitted.
The credentials: true setting allows the frontend to include authentication cookies or Authorization headers. The maxAge value caches preflight responses for 24 hours to improve performance.
Try this: Check your browser's Network tab during a cross-origin request — you'll see the preflight OPTIONS request and the CORS headers in the response.
This configuration solves the immediate problem but creates new ones for development. Local development typically runs on localhost:3000, which isn't in the allowed origins list. The team needs environment-specific CORS settings that are permissive locally but strict in production.
// Environment-aware CORS configuration
const isDevelopment = process.env.NODE_ENV === 'development';
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, Postman)
if (!origin) return callback(null, true);
const allowedOrigins = isDevelopment
? ['http://localhost:3000', 'http://localhost:3001']
: ['https://dashboard.apiforge.com', 'https://app.apiforge.com'];
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS policy violation'));
}
},
credentials: true
};What just happened?
The origin function runs for each request, checking the requesting domain against environment-specific allowed lists. Development allows localhost origins while production restricts access to official APIForge domains.
The function also handles requests with no origin header — these come from mobile apps, API clients like Postman, or server-to-server requests that don't trigger CORS at all.
Try this: Test your CORS configuration with curl or Postman — these tools don't send Origin headers, so they bypass CORS entirely.
Common CORS Pitfalls
CORS errors are notoriously confusing because browsers provide vague error messages that don't explain the actual problem. Understanding the most common failure modes saves hours of debugging time.The "has been blocked by CORS policy" error appears in dozens of variations, but the root cause usually falls into one of five categories. Missing Access-Control-Allow-Origin headers. Mismatched origins in development vs production. Attempting to use wildcards with credentials. Forgetting to handle preflight OPTIONS requests. Trying to read response data from failed CORS requests.
One particularly frustrating scenario happens when your API returns a 500 error but your JavaScript sees a CORS error instead. The server error triggers first, but without proper CORS headers in the error response, the browser blocks access to the error details. Your code can't even read the status code or error message.
CORS Credential Trap
Setting Access-Control-Allow-Origin to * while Access-Control-Allow-Credentials is true causes an immediate CORS failure. Browsers reject this combination as a security violation. You must specify exact origins when allowing credentials.
The preflight cache creates another class of problems. Browsers cache successful preflight responses based on the Max-Age header, but they don't cache failures. This means a misconfigured server might work intermittently — failing on the first request when preflight is required, then succeeding on subsequent requests when preflight is cached.
Development environments compound these issues. Your localhost frontend making requests to a local API server works fine, but deploying to staging breaks everything because the origins change. HTTPS certificates, subdomain configurations, and port numbers all affect CORS behavior in ways that aren't obvious during local development.
CORS Security Considerations
CORS exists to prevent malicious websites from making unauthorized requests to other domains, but configuring it incorrectly can create security vulnerabilities rather than preventing them.The wildcard origin (*) disables CORS protection entirely. Any website can make requests to your API, potentially exposing user data or triggering unwanted actions. This might seem acceptable for public APIs, but even read-only endpoints can leak information about API structure, rate limits, or error handling that assists attackers.
Allowing credentials amplifies these risks dramatically. Authenticated requests carry user context — session cookies, authorization tokens, or client certificates that identify specific users. A malicious site that tricks users into visiting while logged into your application could perform actions on their behalf.
The null origin presents a particular challenge. Browsers send Origin: null for requests from file:// URLs, sandboxed iframes, or redirected requests. Some developers allow null origins to support local testing, but this also allows data: URLs and other potentially dangerous contexts.
Dynamic Origin Validation
Instead of hardcoding allowed origins, consider validating them against a database or configuration service. This allows you to add new origins without deploying code changes, but be cautious about who can modify the allowed origins list.
Subdomain wildcards create another security consideration. While CORS doesn't support subdomain wildcards directly, some developers implement custom logic to allow any subdomain of their domain. This works until someone registers a similar domain name or finds a way to create unauthorized subdomains.
The principle of least privilege applies strongly to CORS configuration. Only allow the specific origins, methods, and headers your application actually needs. Audit these settings regularly as your application evolves and remove any permissions that are no longer required.
Debugging CORS Issues
Effective CORS debugging requires understanding what browsers actually send and receive, not just what your JavaScript code attempts to do.Browser developer tools reveal the complete CORS conversation. Open the Network tab and look for OPTIONS requests — these are your preflights. Failed preflight requests prevent the actual request from ever being sent. Successful preflights show you exactly which headers the server returned and whether they match what your request needs.
The Console tab shows CORS errors, but these messages are often misleading. A "CORS policy" error might actually be caused by a network failure, a 500 server error, or even a typo in your URL. The Network tab shows what actually happened at the HTTP level.
// APIForge debugging utility for CORS issues
async function debugCorsRequest(url, options = {}) {
console.log('🔍 CORS Debug Request:', {
url,
method: options.method || 'GET',
headers: options.headers || {},
credentials: options.credentials || 'same-origin'
});
try {
const response = await fetch(url, options);
console.log('✅ CORS Success:', {
status: response.status,
headers: Object.fromEntries(response.headers.entries())
});
return response;
} catch (error) {
console.error('❌ CORS Failure:', {
message: error.message,
type: error.constructor.name
});
throw error;
}
}
// Test APIForge API endpoint
debugCorsRequest('https://api.apiforge.com/user/profile', {
method: 'GET',
headers: { 'Authorization': 'Bearer token123' },
credentials: 'include'
});What just happened?
The debug utility logs both the request configuration and the response headers, making CORS issues immediately visible. You can see whether the server is returning the expected CORS headers and whether they match your request requirements.
This approach catches common problems like missing credentials in the request, incorrect origin headers, or server-side CORS configuration errors before they become confusing browser error messages.
Try this: Use this pattern in your development environment to verify CORS configuration before deploying to production.
Server-side logging complements client-side debugging. Log incoming OPTIONS requests and the CORS headers you're sending in response. Many CORS issues stem from servers that handle regular requests correctly but fail to process preflight requests or return the wrong headers.
Testing with curl bypasses CORS entirely and helps isolate server-side issues from browser-side restrictions. If curl works but your browser doesn't, you know the problem is in CORS configuration rather than API functionality.
Quiz
1. The APIForge frontend at https://app.apiforge.com needs to make requests to https://api.apiforge.com. What must happen for these requests to work?
2. When a browser encounters a cross-origin PUT request with JSON data, what does it do first?
3. Your APIForge API needs to accept authenticated requests from multiple frontend applications. Why can't you set Access-Control-Allow-Origin to * and Access-Control-Allow-Credentials to true?