Why Your API Works in curl but Fails in the Browser: A CORS Debugging Guide

Why Your API Works in curl but Fails in the Browser: A CORS Debugging Guide

Trace a failing browser request through its origin, preflight, credentials and response headers without hiding the error with no-cors.

An API can work in curl and still fail in a browser because the browser applies cross-origin access rules that curl does not enforce. A successful command-line response proves the endpoint is reachable from that client. It does not prove that a web page on another origin is allowed to read the response.

Start with the failing request in the browser's Network panel. Keep its method, full URL, request headers, response status and console message together. Changing server settings before identifying the failing stage often turns one clear problem into several confusing ones.

1. Compare the origins, not just the domain names

An origin includes scheme, hostname and port. A page at https://app.example.com calling https://api.example.com is cross-origin. So is a development page on one localhost port calling an API on another. Write down both origins exactly; an allowlist entry for an old port will not match the current development page.

2. Look for an OPTIONS request

A browser may send a preflight request before the actual request. For example, a JSON POST or a request carrying an Authorization header can require preflight. If OPTIONS fails, the application request may never be sent. Inspect the preflight response independently instead of searching only for your POST in server logs.

A fictional development setup might allow the origin http://localhost:5173, the POST method and the Content-Type header. Those values should describe the actual intended client, not a collection of guessed headers. The server must return the appropriate CORS headers on the actual response as well.

3. Distinguish authentication from preflight

If your authentication middleware rejects every unauthenticated OPTIONS request, it may prevent the browser from reaching the authenticated application request. Route preflight through the server's intended CORS handling. Keep authorization checks on the actual operation; allowing a preflight is not permission to read private data.

ObservationNext check
OPTIONS failsAllowed origin, method, headers and middleware order
POST returns 401Credentials and authorization on the real request
Only error responses show a CORS messageCORS headers on exception and gateway responses
Only one browser profile failsExtensions, stored state and the exact request difference

4. Treat credentialed requests explicitly

For a credentialed cross-origin response, a wildcard allowed origin is not sufficient. The response needs the appropriate explicit origin and credential allowance. Cookie delivery also has its own rules, so adding a CORS header cannot by itself make a cookie appear. The MDN CORS guide documents preflight and credential requirements.

5. Do not hide the symptom with no-cors

Changing fetch to no-cors does not give application JavaScript normal access to a cross-origin response. It can leave you with an opaque response whose body cannot be read as the JSON your application expects. Fix the intended client-server policy rather than treating the absence of a visible exception as success.

A minimal regression check

Test one allowed origin and one disallowed origin. Exercise a successful response, an authentication failure and a server error. Confirm that private content remains protected even when requests come from non-browser clients: CORS is not an authentication system.

Record the working request and the reason for each allowed origin in the project. If the endpoint is cached, review how responses vary by origin alongside the HTTP caching guide. If the failure is actually rate limiting, use the separate 429 retry guide rather than broadening CORS permissions.

Related