Blocked by CORS Policy in Spring Boot – Fix CORS Correctly

  • Last Updated: October 5, 2026
  • By: javahandson
  • Series
img

Blocked by CORS Policy in Spring Boot – Fix CORS Correctly

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.

1. The exact error message you see

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.

2. What CORS really means in plain words

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:

  • a scheme, like http or https
  • the host, such as localhost or example.com
  • a port number, like 3000 or 8080

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.

2.1 The CORS handshake step by step

It helps to picture the whole flow once. Here is what happens behind the scenes on a cross-origin call:

  • Your JavaScript calls the API that sits on another origin.
  • For a non-simple request, the browser first sends an OPTIONS preflight.
  • The server replies with Access-Control headers that list what is allowed.
  • Then the browser checks those headers against your request.
  • If everything matches, the real request goes through.
  • When something is missing, the browser blocks it and logs the CORS error.

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.

2.2 Why the browser blocks the call, not the server

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.

2.3 Simple requests versus preflight requests

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.

2.3.1 What triggers a preflight

A preflight fires when your request is not “simple”. Common triggers include:

  • methods like PUT, PATCH, or DELETE
  • a custom header such as Authorization or X-Auth-Token
  • a Content-Type of application/json

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.

3. A minimal example that reproduces the error

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.

3.1 Why it works in Postman but fails in the browser

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.

4. Why the error happens in Spring Boot

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.

5. How to fix CORS correctly in Spring Boot

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.

5.1 Fix 1 – Use @CrossOrigin on a controller

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.

5.2 Fix 2 – Global CORS with WebMvcConfigurer

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.

5.3 Fix 3 – Define a CorsConfigurationSource bean

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.

5.4 Fix 4 – Enable CORS with Spring Security

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.

5.4.1 A quick note on the OPTIONS status code

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.

5.5 Fix 5 – Handle allowed origins and credentials correctly

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 startup

If 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.

5.6 Which CORS fix should you pick?

With five options on the table, the choice can feel confusing. Here is a simple rule of thumb:

  • Use @CrossOrigin for a tiny app or a quick prototype.
  • Pick WebMvcConfigurer when you have no Spring Security and want one global rule.
  • Choose a CorsConfigurationSource bean when Spring Security is in play.
  • Switch to allowedOriginPatterns whenever you need wildcards with credentials.

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.

5.7 Do not ship localhost to production

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.

6. Common mistakes that keep breaking CORS

Even after a fix, CORS can still fail. Usually, a small detail is off somewhere. Here are the traps I see most often:

  • Mixing allowedOrigins(“*”) with allowCredentials(true), since that combo is rejected.
  • Forgetting to enable CORS inside Spring Security, so the preflight gets blocked first.
  • Allowing GET and POST but not OPTIONS, PUT, or DELETE, which breaks the preflight.
  • A trailing slash mismatch, like http://localhost:3000/ versus http://localhost:3000.
  • Confusing the ports, because your frontend origin is the caller, not the backend.
  • Expecting Postman to show the error, which it never will.

Go through this list first. Nine times out of ten, your bug is hiding right here.

7. How to verify the fix worked

Do not just reload the page and hope for the best. Instead, check the headers directly. There are two reliable ways to do that.

7.1 Check with curl by simulating a preflight

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.

7.2 Check in the browser DevTools

The browser gives you a second view. Open DevTools and go to the Network tab. Trigger the API call again, and then do this:

  • Find the OPTIONS request and confirm it returns 200.
  • Click the real request and open its Response Headers.
  • Look for Access-Control-Allow-Origin with your frontend origin.

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.

8. FAQ’s on CORS policy in Spring Boot

Q: Why does my API work in Postman but fail with a CORS error in the browser?

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.

Q: What is a CORS preflight request in Spring Boot?

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.

Q: Why does CORS still fail after I add Spring Security?

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.

Q: Can I use allowedOrigins(“*”) together with credentials?

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.

Q: What is the difference between allowedOrigins and allowedOriginPatterns?

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.

9. Conclusion

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.

9. Further Reading

Leave a Comment