React · Spring Boot
How to fix CORS errors between React and Spring Boot
Fix “blocked by CORS policy” errors between a React front end and a Spring Boot API: MVC config, Spring Security preflight, dev proxies, credentials and debugging.
The request works in Postman. In the browser, your React app gets this instead:
Access to fetch at 'http://localhost:8080/api/orders' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.Nothing is wrong with your fetch code. The browser is enforcing a security rule that Postman ignores, and the fix belongs on the Spring Boot side — or in how you serve the two apps.
Why it happens
Browsers block JavaScript from reading responses from a different origin — a different scheme, host or port — unless the server explicitly allows it with CORS headers. localhost:5173 (your React dev server) and localhost:8080 (Spring Boot) are different origins, so the browser asks Spring Boot for permission first.
For many requests — anything sending JSON with Content-Type: application/json, or an Authorization header — the browser first sends a preflight OPTIONS request. If that preflight doesn’t come back successful with the right headers, the real request is never sent.
Fix 1: allow your front end in Spring MVC
For a single controller, @CrossOrigin(origins = "http://localhost:5173") is enough. For the whole API, configure it once:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173", "https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true);
}
}Fix 2: if you use Spring Security
This is why “I added @CrossOrigin and it still fails” happens. Spring Security’s filters run before Spring MVC, and they reject the unauthenticated preflight with a 401 or 403 before your CORS settings are ever consulted. Enable CORS in the security chain and give it a configuration source:
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173", "https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}With cors(Customizer.withDefaults()), Spring Security picks up the corsConfigurationSource bean and answers preflight requests before authentication runs.
Fix 3: avoid CORS in development with a proxy
If the browser only ever talks to one origin, there is no CORS at all. Let your dev server forward API calls to Spring Boot:
export default {
server: {
proxy: {
"/api": "http://localhost:8080",
},
},
};Then call fetch("/api/orders") with a relative URL. (Create React App projects use the proxy field in package.json for the same effect.) In production, serving the React app and the API under the same domain — through a reverse proxy or Kubernetes Ingress — removes the need for CORS in the same way.
Cookies and credentials
- To send cookies cross-origin, the browser needs
credentials: "include"on the fetch call andallowCredentials(true)on the server. - With credentials allowed, the allowed origin can’t be
"*"— Spring rejects that combination at startup. List your origins, or useallowedOriginPatternsif you need wildcards such ashttps://*.example.com. - Cross-site cookies also need
SameSite=None; Secure, which means HTTPS.
Debugging checklist
- Open DevTools → Network, find the
OPTIONSpreflight and check its status. A 401, 403 or 404 there means the server is rejecting the preflight — usually Spring Security. - Compare the origin exactly:
httpvshttps,localhostvs127.0.0.1, and the port all matter. - Look at the real response status. A 500 error often comes back without CORS headers, so the browser reports a CORS error while the actual problem is an exception on the server — check the Spring logs.
- Make sure every custom header you send (for example
X-Request-Id) is in the allowed headers.