Blocked by CORS Policy in Spring Boot – Fix CORS Correctly
-
Last Updated: October 5, 2026
-
By: javahandson
-
Series

Seeing “blocked by CORS policy in Spring Boot”? Learn why it happens and fix it correctly with @CrossOrigin, WebMvcConfigurer, and Spring Security CORS.
“Blocked by CORS policy in Spring Boot” is one of those errors that stops you cold. Your backend runs. Your endpoint returns clean data in Postman. Yet the browser refuses to show it. Instead, the console throws a long red warning about a blocked cross-origin request. I have watched this trip up both juniors and seniors on real projects.
So let us fix it the right way. We will not use a random copy-paste that quietly opens security holes. First, we will read the exact error. Next, we will understand what CORS really does. Then we will reproduce the problem with a tiny example. Finally, you will get five clean fixes in priority order. They cover Spring MVC CORS, Spring Security CORS, preflight requests, and allowed origins.
When the browser blocks a call, the console usually shows something like this:
Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Sometimes the wording shifts a little. You might see a preflight variant instead:
Access to XMLHttpRequest at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Both messages point to the same root cause. The server never told the browser that this origin is allowed. So the browser throws the response away.
Here is a detail that confuses many people. This error comes from the browser, not from Spring Boot. Your server may have already handled the request fully. Still, the browser hides the response because the permission headers are missing.
CORS stands for Cross-Origin Resource Sharing. It is a safety rule built into every modern browser. In short, the rule decides which websites may read responses from which servers.
An “origin” is really made of three parts:
If even one part differs, the two URLs count as different origins. For example, http://localhost:3000 and http://localhost:8080 do not match. The port is different, so the browser treats them as strangers.
It helps to picture the whole flow once. Here is what happens behind the scenes on a cross-origin call:
Notice the order here. The permission check happens before your data ever reaches the page. That is why fixing the headers on the server side solves the problem for good.
By default, a browser lets a page call its own origin freely. A call to a different origin becomes a “cross-origin” call. For safety, the browser will not let JavaScript read that response unless the server agrees. The server agrees by sending special headers, and the main one is Access-Control-Allow-Origin.
No header means no permission. As a result, the browser blocks the read and logs the CORS error.
Not every cross-origin call behaves the same way. The browser splits them into two types.
A simple request goes straight to the server. It uses GET, POST, or HEAD with only basic headers. The browser sends it first, then checks the response headers afterward.
A preflight request works differently. Before the real call, the browser sends a small OPTIONS request. This is a quick permission check. Basically, the browser asks, “Am I allowed to send this?” The server must answer with the right headers. Only then does the real request go out.
A preflight fires when your request is not “simple”. Common triggers include:
Most REST calls from React or Angular send JSON. Therefore, you will hit preflight requests very often. That is exactly why a broken OPTIONS response causes this error so frequently.
Let us build the smallest setup that breaks. Imagine a React app on port 3000 and a Spring Boot API on port 8080.
Here is the frontend call:
// React app running on http://localhost:3000
fetch("http://localhost:8080/api/users")
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));And here is the Spring Boot controller:
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/users")
public List<String> getUsers() {
return List.of("Asha", "Ravi", "Meera");
}
}Now run both apps and open the React page. The call fails, and the console shows the CORS error. However, if you hit http://localhost:8080/api/users directly in Postman, it works fine. That gap between Postman and the browser is the classic sign of a CORS problem.
This question comes up again and again. The answer is simple. Postman is not a browser. Tools like Postman and curl do not enforce CORS at all. Only browsers apply the rule, so only browsers show the block.
By default, Spring Boot does not add CORS headers to its responses. There is no Access-Control-Allow-Origin header in the reply. So when a different origin calls the API, the browser finds no permission and blocks it.
In short, the server stays silent. And silence means “not allowed” in the CORS world. Your job is to make Spring Boot speak up and declare which origins are welcome.
Many developers assume the framework handles this on its own. It does not, and that is by design. A framework cannot guess which domains should trust your API. Therefore, you have to state the allowed origins yourself.
| 💡 Interview Insight Interviewers love to ask why an API works in Postman but fails in the browser. The answer is CORS. Postman is not a browser, so it skips CORS checks entirely. Browsers enforce them, while tools like curl and Postman simply ignore them. |
There are several ways to fix this. We will move from the quickest option to the most complete one. Pick the approach that matches your project size and your security needs.
The fastest fix is the @CrossOrigin annotation. You add it to a controller or even a single method.
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
@GetMapping("/users")
public List<String> getUsers() {
return List.of("Asha", "Ravi", "Meera");
}
}This tells Spring to allow calls from that one origin. It works well for small apps or quick demos. However, it spreads CORS rules across many files as your app grows. For one or two controllers, it is fine. A big project, though, deserves a single global setup.
In most apps, a global configuration is far cleaner. You define the rules once, in a single place.
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}Now every endpoint under /api follows the same rule. This keeps your CORS policy in Spring Boot tidy and easy to change. When you move to production, you simply swap the allowed origin for your real domain.
Sometimes you need finer control. A CorsConfigurationSource bean gives you that. It also plugs neatly into Spring Security, which we will cover next.
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}This bean holds your full CORS policy in one object. Other parts of Spring can then reuse it. For larger apps, it is the most flexible choice.
Here is where many developers get stuck. You add CORS config, yet the error stays. The reason is Spring Security. Once Security is on the classpath, it sits in front of your controllers. As a result, it can block the preflight OPTIONS request before your CORS config ever runs.
So you must tell Spring Security to use your CORS settings. You do that inside the security filter chain.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated());
return http.build();
}
}That .cors(Customizer.withDefaults()) line is the key. It tells Security to pick up your CorsConfigurationSource bean from Fix 3. Together, they let the preflight pass and then apply your rules.
| 💡 Interview Insight A common senior-level question: why does CORS fail only after adding Spring Security? Because the security filter chain runs before the controller. If Security does not enable CORS, it rejects the OPTIONS preflight early, and the browser reports a CORS error. |
People often ask what the preflight should return. A healthy preflight returns 200 or 204. Both codes tell the browser that the check passed. A 401 or 403 on the OPTIONS call almost always points back to Spring Security. In that case, revisit Fix 4 and confirm that CORS is enabled in the filter chain.
This last fix saves you from a subtle trap. You cannot use a wildcard origin together with credentials. If you set allowCredentials(true) and allowedOrigins(“*”), Spring fails fast at startup. The browser also refuses that mix.
When you send cookies or an Authorization header, you need credentials. In that case, list exact origins instead of the wildcard.
// Works: exact origin with credentials
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowCredentials(true);
// Fails: wildcard with credentials
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(true); // rejected at startupIf you really need a pattern, use allowedOriginPatterns instead. It supports wildcards even when credentials are on.
config.setAllowedOriginPatterns(List.of("http://localhost:*"));
config.setAllowCredentials(true);Now any localhost port can connect during development, while cookies still flow correctly. For production, you would narrow this down to your trusted domains only.
With five options on the table, the choice can feel confusing. Here is a simple rule of thumb:
For real projects, I usually combine Fix 3 and Fix 4. That pair keeps the config in one bean and wires it cleanly into Spring Security.
One more warning before you deploy. The localhost origins above are only for development. In production, your frontend lives on a real domain. So replace http://localhost:3000 with something like https://app.example.com. Keep the list short and specific, and never leave a bare wildcard on a secured API.
Even after a fix, CORS can still fail. Usually, a small detail is off somewhere. Here are the traps I see most often:
Go through this list first. Nine times out of ten, your bug is hiding right here.
Do not just reload the page and hope for the best. Instead, check the headers directly. There are two reliable ways to do that.
You can mimic the browser’s preflight using curl. Send an OPTIONS request and read the response.
curl -i -X OPTIONS http://localhost:8080/api/users \ -H "Origin: http://localhost:3000" \ -H "Access-Control-Request-Method: GET"
A correct setup returns a 200 response with these headers:
Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: GET, POST, PUT, DELETE
If you see those lines, your server now grants permission. If they are missing, the config is not wired correctly yet.
The browser gives you a second view. Open DevTools and go to the Network tab. Trigger the API call again, and then do this:
When both the preflight and the real call carry the right headers, the error finally disappears. After that, your frontend reads the data without any trouble.
A: CORS is a browser-only rule. Postman and curl do not enforce it, so they call your API freely. A browser, however, needs the server to send an Access-Control-Allow-Origin header. When Spring Boot does not send it, the browser blocks the response and logs the CORS error, even though the server returned data.
A: A preflight is a small OPTIONS request the browser sends before the real call. It fires for non-simple requests, such as PUT, DELETE, or any call that sends JSON or an Authorization header. The server must answer the preflight with the right Access-Control headers. Only then does the browser send the actual request.
A: The Spring Security filter chain runs before your controllers. If Security does not have CORS enabled, it rejects the OPTIONS preflight early, often with a 401. Add .cors(Customizer.withDefaults()) to the security filter chain and expose a CorsConfigurationSource bean so Security applies your CORS rules.
A: No. A wildcard origin cannot be combined with allowCredentials(true). Spring fails at startup and the browser also refuses the mix. If you send cookies or an Authorization header, list exact origins instead, or use allowedOriginPatterns, which supports wildcards even with credentials.
A: allowedOrigins expects exact origins like http://localhost:3000 and does not allow wildcards with credentials. allowedOriginPatterns supports patterns such as http://localhost:* and still works when credentials are enabled. Use patterns for flexible development setups, but narrow them to trusted domains in production.
CORS feels scary at first, but it follows a simple logic. The browser just wants the server’s permission. Once Spring Boot sends the right headers, the block goes away. So start with the smallest fix, test it with curl, and tighten your origins before you ship.