Understanding CORS for API Developers | PostPilotUnderstanding CORS for API Developers
A valid fetch call may return a 200 OK status on the network tab but trigger a browser error:<br>Access to fetch at 'https://api.example.com/users' from origin 'http://localhost:3000'<br>has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present<br>on the requested resource.<br>To resolve this, you must understand the interaction between the browser's security policies and server response headers.<br>Same-Origin Policy (SOP)<br>Same-Origin Policy (SOP) is the browser security mechanism that restricts how JavaScript on one origin interacts with resources from another. An origin is the combination of:<br>ComponentExampleProtocolhttps://Domainapi.example.comPort:443All three must match. If any differ, the request is cross-origin and subject to SOP.<br>FromToSame origin?http://localhost:3000http://localhost:8080❌ Different porthttps://app.comhttps://api.app.com❌ Different subdomainhttps://app.comhttp://app.com❌ Different protocolSOP prevents malicious sites from exploiting the fact that browsers automatically attach credentials (cookies) to requests. If you are logged into bank.com, the browser sends your session cookies with every request to that domain. Without SOP, a malicious site could trigger a background request to bank.com/transfer and the browser would include your credentials, authorizing the action without your knowledge.<br>Simple vs Non-Simple Requests<br>Browsers handle cross-origin requests based on their complexity.<br>Simple Requests<br>A request is "simple" if:<br>The method is GET, POST, or HEAD.<br>Headers are limited to Accept, Accept-Language, Content-Language, or Content-Type (with values text/plain, multipart/form-data, or application/x-www-form-urlencoded).<br>It uses no custom headers.<br>Simple requests are sent directly to the server without a handshake.<br>Non-Simple Requests<br>Any request that falls outside the simple criteria is "non-simple." Examples include:<br>Methods: PUT, DELETE, PATCH.<br>Content-Type: application/json.<br>Custom headers: Authorization, X-Api-Key.<br>Non-simple requests trigger a preflight OPTIONS request. The browser verifies server permission before sending the actual request.<br>Response Enforcement<br>The browser does not block a request from leaving the machine or reaching the server. It blocks JavaScript access to the response .<br>Simple Request Flow<br>Browser sends request.<br>Server responds.<br>Browser checks for Access-Control-Allow-Origin in the response.<br>Match : JavaScript can read the response.<br>No match : Browser blocks access.
Non-Simple Request Flow<br>Browser sends OPTIONS preflight.<br>Server responds with CORS headers.<br>Invalid/Missing : Browser stops; actual request is never sent.<br>Valid : Browser sends the actual request.
Server responds to actual request.<br>Browser checks CORS headers again.<br>Match : JavaScript can read the response.<br>No match : Browser blocks access.
CORS Headers<br>Cross-Origin Resource Sharing (CORS) is the mechanism servers use to opt-in to cross-origin access.<br>Standard Response Header<br>Access-Control-Allow-Origin: https://app.com<br>Use * only for public APIs without credentials.<br>Preflight Headers<br>OPTIONS responses for non-simple requests must include:<br>Access-Control-Allow-Methods: GET, POST, PUT, DELETE<br>Access-Control-Allow-Headers: Authorization, Content-Type<br>Credentials<br>If the request includes cookies or Authorization, the server must specify the exact origin and set:<br>Access-Control-Allow-Credentials: true<br>The wildcard * is not permitted when credentials are included.<br>Debugging with API Clients<br>SOP is a browser-only restriction. Tools like curl , Postman , or PostPilot bypass these checks because they are not browsers. If a request works in PostPilot but fails in a web app, the issue is a CORS misconfiguration on the backend.<br>Step-by-Step Debugging<br>1. Identify Request Type<br>Determine if your request is simple or non-simple. If it includes Authorization or application/json, you must debug the preflight first.<br>2. Verify Preflight (Non-Simple Only)<br>Manually trigger the preflight using curl:<br>curl -X OPTIONS https://api.example.com/users \<br>-H "Origin: http://localhost:3000" \<br>-H "Access-Control-Request-Method: GET" \<br>-H "Access-Control-Request-Headers: Authorization" \<br>-v<br>Ensure the response contains the required Access-Control-Allow-* headers. In PostPilot , paste the curl command directly into the URL field ; it automatically parses the command into a request. Send it and inspect the Headers tab to verify the response.<br>3. Verify Actual Response<br>Ensure the final response (not just the preflight) also contains Access-Control-Allow-Origin.<br>Solutions by Use Case<br>First-Party: Proxy (Same Origin)<br>If you control both the frontend and backend, the standard solution is to serve them from the same origin. By using a reverse proxy (e.g., Nginx) or a framework proxy (e.g., Nuxt/Vite dev proxy), you can map the backend to a relative path.<br>Frontend : https://app.com<br>Backend API : https://app.com/api<br>Since they...