Why API requests fail due to CORS and how it breaks the product
A CORS error almost never arrives as a clear diagnostic — the browser simply blocks the response, and the console keeps a dry line about a missing header.
For the product, it looks different: the login form "spins" with no response, the cart doesn't pull in stock levels, the integration with an external service silently returns nothing.
The user thinks the service is broken, even though the backend worked correctly and returned the data — the browser discarded it at the origin policy check stage.
The second typical failure is a rejected preflight request. Before a "complex" request to the API, the browser separately asks the server for permission, and if the response lacks the required headers, the actual call never happens.
Hence the classic picture: everything works in the test environment, but in prod API requests fail because the origin policy is configured for a specific domain and doesn't account for the production address or subdomain.
Environment mismatch in CORS configuration is the most expensive category of such failures: it doesn't reproduce locally and surfaces only at the client's end.
The cost isn't in the error itself, but in how it's investigated. While the team figures out whether it's the server, the proxy, or the frontend, the release is stalled and some users leave.
Let's break down how CORS configuration is closed at the config level so that such failures never reach production.
What correct CORS configuration delivers: the effect in numbers
Correct CORS configuration eliminates an entire class of failures that otherwise surfaces only in production: the browser blocks the request, the frontend receives an empty response instead of data, and the backend logs are clean — the request simply never reaches the server.
Diagnosing such an incident takes hours, because the symptom is visible only in a specific user's browser console, not in API monitoring. When the policy is stated explicitly — as a list of allowed origins, methods, and headers — this scenario ceases to exist as a class.
Integration time shrinks predictably.
While rules are handed out at random — "let's allow everything with a wildcard, we'll sort it out later" — every new environment, subdomain, or individual client turns into a separate round of debugging: the frontend developer and the backend engineer take turns checking whose header didn't arrive.
A consistent policy removes these iterations: a new origin is added with a single configuration edit rather than an investigation.
API stability grows because permissions stop being universal. Targeted rules keep foreign origins off the backend, and preflight requests are cached by the browser — repeated calls to the same endpoints proceed without an extra round of negotiation. This is noticeable on heavily loaded screens, where a single render pulls a dozen requests.
Releases ship faster because policy checks become part of the normal pipeline: the rules live in the repository alongside the code, go through review, and roll out together with the application.
No one edits them by hand on the production environment at deployment time, and a rollback restores both the code and the settings at once.
It is precisely at the intersection of these effects — fewer incidents, less manual work, smoother releases — that it becomes clear that CORS configuration is not a formality but part of a product's fault tolerance.
Approaches to CORS configuration: comparing the options
Configuring CORS is always a matter of choosing the point at which the application receives the permissive headers.
They can be served on the API server itself, injected at an intermediate layer, or the issue can be removed architecturally so that the browser never even sees a cross-domain request.
Below are four working options that teams realistically choose between on projects, with the honest limitations of each.
| Option | When it fits | Limitations |
|---|---|---|
| Permissive headers on the API server | Your own backend, a limited list of frontend domains, cookies and authorization are needed | The domain list must be maintained manually; with multiple environments it's easy to end up out of sync; a mistake in the headers is only visible in the browser |
| Proxying through your own domain | A third-party API with no access to its settings, a quick workaround for browser restrictions | The proxy becomes a point of failure and requires resources; latency grows; authorization and rate limits must be handled separately |
| An API gateway in front of the services | Multiple services, unified access rules, a centralized policy is needed | Requires infrastructure and maintenance; overkill for a single service; the gateway rules must be tested separately from the code |
| Manual header management in code | A prototype, an internal tool, a one-off request | Logic duplicated in every service; rules get lost during refactoring; doesn't scale to a team |
Permissive headers on the server are the baseline option: they give precise control over which domains and which methods are allowed to make requests, and they work correctly with session cookies.
Proxying comes to the rescue when a third-party API can't be touched: the request goes to your domain, and the server forwards it onward — the browser sees an ordinary request with no cross-domain check.
An API gateway makes sense when there is more than one service: access rules are described once and applied to all of them, rather than spreading across configurations.
Manually editing headers is only good as a temporary solution — it doesn't survive team growth or the very first refactoring.
In practice, the choice depends on who owns the API and how many environments need to be supported. Let's break down your request flow — we'll come back with an assessment of which option will solve the task without an extra layer.
Implementation stages: from request audit to handover of the regulation
Setting up CORS is not a single edit in the server config, but a sequence of steps where each subsequent one builds on the previous. If you skip the audit and immediately set the headers, some requests will break, while others will remain open wider than necessary. That is why we work in stages with verification at each one.
- Request audit. We collect all sources from which the frontend accesses the API: domains, subdomains, developers' local addresses, third-party widgets. We record the methods, headers, and scenarios involving credentials — these are the ones that most often break under an incorrect policy.
- Policy design. We determine which sources we explicitly allow, which methods and headers we permit, whether credentials are needed, and how we handle preflight requests. Instead of the "allow everyone" template, we get a list of rules tailored to real scenarios.
- Configuration. We transfer the rules to the server and to the intermediate layer: response headers, preflight request handling, caching of permissions. Separately, we check that the policy does not conflict with the proxy and the load balancer.
- Scenario verification. We run through real chains: login, file uploads, requests with credentials, calls from third-party domains. We check that what is allowed passes, and what is forbidden is rejected with a clear error.
- Documentation. We describe the current rules, the reasons for each permission, and the procedure for adding a new source — so that the next edit does not turn into archaeology.
- Handover. We deliver the regulation to the team, and record the responsible parties and control points. The policy becomes part of the process, not a one-off patch.
This order eliminates the class of errors where the browser blocks a request and the cause is not obvious. Describe your task — we will come back with an estimate and a plan by stages.
What the client receives: configurations and regulations
A project is closed not with a presentation, but with a set of artifacts that another engineer can use to bring up the environment and reproduce the setup without us.
Below is what exactly we hand over as a result of work on a web application and its interaction with external services.
- Configuration files — working configs for each environment (development, test, production) with an explicit list of allowed origins, methods, and headers. We separately record which values are set by environment variables and which are hardcoded deliberately.
- Interaction diagrams — diagrams of request flows between the frontend, backend, and external APIs: who initiates, where the preflight request occurs, at which node the access policy takes effect.
- Access policies — the rules by which a domain is added to the allowlist, the procedure for adding a new origin, and the requirements for verifying it. This eliminates the situation where a change is made by whoever gets to the config first.
- Deployment and rollback instructions — a step-by-step procedure for applying changes with rollback points: if a new policy breaks an integration, rollback takes minutes, not hours of searching for the cause.
- Verification checklists — a set of scenarios that are run before deployment: a request from an allowed domain, from a foreign one, a preflight request, a request with credentials. Each item — with an expected result.
- Response procedure — what to do when an access error appears in the user's browser: what data to collect, where to look at the logs, whom to escalate to.
- Policy change log — who, when, and why expanded the list of origins. Six months later, this is the only way to understand why access is open to a particular domain.
The package is designed so that the client's team can maintain the project on their own: configurations are readable, diagrams reflect the actual state, checklists are reproducible.
Describe your task — we will come back with an estimate and a plan for the extent to which these artifacts cover your scope.
Case study: how we eliminated request failures and accelerated releases
The client — a service with a personal account area and a mobile app — came to us with a symptom the team couldn't catch for months: some API requests were failing with a browser security policy error, but only for half the users and only in production.
Everything worked on staging, and for the developers too. Reproducibility was zero, while support complaints kept growing.
What we found
The cause turned out to be a combination of factors.
The reverse proxy returned different headers for requests with and without Origin, and the OPTIONS preflight request on some routes went to the application instead of the proxy and returned a redirect — the browser doesn't accept such a response and blocks the main request.
On top of that, the list of allowed origins was hardcoded in the code and didn't cover the new subdomain that had been added for promotions. Individually, each factor was harmless; together, they caused intermittent failures.
What we changed
We moved the policy into the proxy configuration: a single list of origins, explicit handling of OPTIONS before it reaches the application, a correct set of Access-Control-Allow-Methods and Access-Control-Allow-Headers matching the methods and headers actually in use.
For requests with credentials, we pinned specific origins instead of a wildcard — the browser doesn't accept a broad wildcard together with session data being passed. Separately, we disabled caching of preflight responses so that policy changes would take effect without waiting.
Metrics before and after
| Metric | Before | After |
|---|---|---|
| Share of requests with a policy error | 6-9% during peak hours | 0 |
| Support tickets about "login doesn't work" | 40+ per week | isolated cases |
| Time to add a new subdomain | 2-3 days of edits and a release | a configuration change in minutes |
| Shipping API changes | cautiously, selectively | standard cycle |
A separate effect was release predictability. Previously, any change to domains or headers dragged along an application release and manual verification; now it's a configuration change that goes through review and can be rolled back with a single line. The team stopped being afraid to touch integrations.
We worked in tandem with their Cloudflare and internal proxy, without changing the application architecture: the changes landed at the level where they belong.
If you have a similar picture — failures that "come and go" and a long path for any change to reach production — describe your task, and we'll get back to you with an estimate and a plan.
How does configuring CORS on a server differ from configuring it on a gateway?
On a server, the policy is set by the application itself: the Access-Control-Allow-Origin, Allow-Methods, and Allow-Headers headers are formed in a handler or middleware and are sent in every response.
This gives maximum flexibility — rules can be built on request data: allow a specific origin only for certain routes, substitute different sets of methods for reads and writes, and keep a log of rejections alongside the business logic.
There is one limitation: the policy lives inside the code, and with multiple services it has to be described in each one, risking discrepancies between them.
A gateway moves that same policy to the level in front of the applications. Rules are described declaratively and applied to all routes at once, uniformly, regardless of what the service is written in.
The preflight OPTIONS request is handled at the entry point and never reaches the application — the backend does not spend resources on service calls.
The flip side: the gateway sees only the external contour of the request and does not know the internal logic, so fine-grained rules such as "origin A — read-only, origin B — write-only" are expressed more coarsely on it.
The key difference is in the point where the policy is applied. On a server it is part of the application and changes along with a release; on a gateway it is infrastructure configuration that is edited separately from the code and rolled out centrally.
This also leads to a different cost of error: an incorrect header in code is fixed by a new service deployment, while an incorrect rule on the gateway is fixed by editing the configuration without rebuilding the applications.
The choice depends on the contour. A single service with several origins — the server-side option is simpler and more precise. Several services behind a common entry point — a gateway removes duplication and maintains a single standard.
Both levels are often used: the gateway closes off the common perimeter and service requests, while the application further tunes the rules for its own routes.
Common technical concerns when configuring CORS
Before changing response headers, it's worth understanding: CORS is not a "server setting" but a negotiated contract between the request origin and the API.
Most concerns arise where the frontend and backend modify their parts independently and verify the result only in the browser — by the appearance of an error in the console.
The solution is already deployed, but requests still fail — what should be checked first?
Look at the preflight request: if it goes out and comes back without the required headers, the main request never reaches the API at all. Next — the processing order: a policy at the reverse proxy level may overwrite what the application sets.
And the third common source is caching: the browser holds the preflight result, so changes aren't visible until it's cleared. We check the entire chain, not just the application code.
How do you avoid conflicts between the proxy, the application, and an external service?
Maintain a single source of truth: the list of allowed origins is defined in one place, and the other layers only pass it through without adding their own values.
If the API serves data to third-party sites, the policy is described separately and explicitly — otherwise the first domain added breaks the rest. Conflicts almost always stem from duplicated rules, not from their content.
How do you verify that the configuration actually works?
An automated test for the preflight request with real origins and methods — it catches regressions before the user does. Plus checking from a browser with a clean profile and from the command line: they reveal different layers.
Separately, we record the behavior on failures — what response the client receives when the origin is not allowed.
What should be done with already working integrations when the policy changes?
Introduce changes in stages: first we expand the list, monitor the requests, and only then remove what's no longer needed. Request logs with rejected origins give an accurate picture of who else is still calling the API. This way the restructuring happens without sudden failures for adjacent teams.
If you still have questions about your situation — describe the current setup: where the API is hosted, who calls it, and how verification is currently arranged. We'll come back with an analysis and a plan of changes.
Let's discuss your task and come back with an estimate and a plan
To come back with an estimate and a plan rather than generalities, we need the context of the task.
Setting up CORS almost never comes down to a single header in the server response — what matters is where the requests come from, which methods and headers are actually used, whether there is authorization via cookie, and how many domains and subdomains are involved. The more precisely you describe the picture, the fewer iterations it will take to reach agreement.
Describe the task in free form — that is enough to get started:
- which domains and subdomains access your API (frontend, mobile app, partner integrations);
- which methods are needed: read only, or also write, delete, preflight requests;
- whether credentials are passed — cookies, tokens in headers, sessions;
- where the error currently occurs: in the browser, in the server logs, only for some users;
- what the frontend and backend stack is, and who is responsible for the server and reverse proxy configuration;
- whether there are constraints: security requirements, regulations, a ban on making changes to a third-party service.
We will study the context, reproduce the behavior on your request scheme, and propose a solution — with a work plan, timelines, and what is needed on your side.
If the task can be solved with a pinpoint policy fix, we will say so directly; if the access scheme needs to be reworked, we will state that honestly as well, without pushing unnecessary scope.
Let's discuss the task — write in any convenient channel and attach examples of requests and responses if you have them. We will come back with an estimate and a plan.







