- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
CORS Explained: Why the Browser Blocks Your API Call (and Postman Doesn't)
About Post
The API works in Postman. It works with curl. The backend developer swears it works. Then the frontend calls it from the browser and the console fills with red:
Access to fetch at 'https://api.example.com/units' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Cue the classic debate. The frontend says the API is broken. The backend says the frontend is broken. Someone suggests a browser extension that "disables CORS". Nobody is quite sure what CORS is.
Let's fix that, because once the mental model clicks, these errors take two minutes to solve instead of an afternoon.
The one sentence that explains everything
CORS is not your server blocking the request. It's the browser refusing to hand the response to your JavaScript.
That's why Postman and curl work: they aren't browsers, so they don't enforce it. In many cases the request actually reached your server, ran, and returned a response. The browser just looked at the response headers, didn't find permission to share it with the page, and threw it away.
Which means CORS is not a firewall. It does nothing to protect your API from scripts, bots or attackers using curl. It protects users, from other websites running code in their browser.
Why browsers do this: the same-origin policy
Imagine you're logged in to your bank in one tab. In another tab you open a random website. If any page could freely call any URL with your cookies and read the response, that random site could fetch your bank statement in the background and send it home.
The same-origin policy stops that. By default, JavaScript can only read responses from the same origin as the page, where origin means scheme + host + port. All three must match:
| Page | Request to | Same origin? |
|---|---|---|
| https://app.example.com | https://app.example.com/api | Yes |
| https://app.example.com | https://api.example.com | No (different host) |
| http://localhost:5173 | http://localhost:8000 | No (different port) |
| http://example.com | https://example.com | No (different scheme) |
That localhost row is why every React or Vue developer meets CORS in week one: the dev server and the API run on different ports.
CORS (Cross-Origin Resource Sharing) is the controlled exception. The server can say, through response headers, "I'm fine with pages from this origin reading my responses."
The headers that matter
Access-Control-Allow-Origin: which origin may read the response. Either one exact origin or*.Access-Control-Allow-Methods: which methods are allowed for cross-origin calls (in a preflight response).Access-Control-Allow-Headers: which request headers the page may send, such asAuthorizationorContent-Type.Access-Control-Allow-Credentials: whether cookies may be included.Access-Control-Max-Age: how long the browser may cache a preflight answer.Access-Control-Expose-Headers: which response headers JavaScript is allowed to read beyond the basic ones (useful for things like pagination headers).
The preflight: the request you didn't send
Here's the part that confuses people most. You send one POST, but the network tab shows two requests, and the first one is an OPTIONS.
For anything that isn't a "simple" request, the browser asks permission first. A request stays simple only if it uses GET, HEAD or POST, sends only basic headers, and has a content type of form data or plain text. In practice, almost every API call fails that test, because Content-Type: application/json and an Authorization header both trigger a preflight.
OPTIONS /api/tickets HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 3600
Only if that answer is a success with the right headers does the browser send the real POST. If the preflight fails, your actual request never leaves the browser.
Credentials: where * stops working
If your frontend sends cookies (for example, a single-page app using Laravel Sanctum's cookie-based auth), two things must be true:
- The client opts in:
fetch(url, { credentials: 'include' }), orwithCredentials: truein Axios. - The server responds with
Access-Control-Allow-Credentials: trueand an exact origin.Access-Control-Allow-Origin: *is not allowed together with credentials. The browser will reject it.
APIs that use bearer tokens in the Authorization header don't need cookie credentials at all, which is one reason token-based mobile and SPA setups often have fewer CORS headaches.
Fixing it in Laravel
Laravel handles CORS for you with the built-in HandleCors middleware, which is enabled by default. In Laravel 12 the config file isn't in your project until you publish it:
php artisan config:publish cors
Then set it to match your frontends:
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [
env('FRONTEND_URL', 'http://localhost:5173'),
],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 3600,
'supports_credentials' => true, // only if you use cookies
];
Run php artisan config:clear (or re-cache) after changing it. A cached config with the old values has fooled more than one developer into thinking their fix didn't work.
The sneaky cases
- The CORS error that's really a 500. If the request dies in a way that skips the CORS headers (a fatal error, a gateway timeout, a web server error page), the browser reports a CORS error instead of the real one. Check the response status in the network tab and your Laravel log before touching CORS settings.
- A path that isn't covered. Your config says
api/*, but the frontend is calling/oauth/tokenor/broadcasting/auth. Add the path. - Preflight hitting auth or a redirect. If something answers the
OPTIONSrequest with a 401 or a 302 to the login page, the preflight fails. This is common when a proxy or web server config intercepts it. - Duplicate headers. Nginx adds
Access-Control-Allow-Originand Laravel adds it too. Two values is invalid, and the browser rejects it. Handle CORS in one place. mode: 'no-cors'isn't a fix. It gives you an "opaque" response that your code can't read. The error disappears, and so does your data.
Debugging rule: open the network tab, find the OPTIONS request first, and read its status and response headers. Most of the time the answer is right there, not in the console message.
Why the "allow everything" fix is a bad habit
Setting allowed_origins to * makes the error go away, and for a truly public, read-only API without cookies, it's fine. For an app with logins, list your real frontends explicitly. CORS isn't your main security layer, but there's no reason to tell every website on the internet that it's welcome to read your API through your users' browsers.
Remember this
- CORS is enforced by the browser, not your server. Postman working proves nothing.
- Origin = scheme + host + port. Different port, different origin.
- JSON bodies and auth headers trigger a preflight
OPTIONSrequest. - Cookies need
credentials: 'include',supports_credentials, and an exact origin. - Configure it in one place:
config/cors.phpin Laravel.
If you'd like the official deep dive, MDN's CORS guide is excellent.
What's the most misleading CORS error you've ever chased, and what turned out to be the real cause?

Be first to comment it...