GuidesJVM & PHP
Fix CORS errors in Spring Boot
A Spring Boot CORS error means the browser withheld a response because the server did not permit the calling origin. The fix lives in one of three layers: a @CrossOrigin annotation, the global MVC configuration, or the Spring Security filter chain, which runs first and can reject preflights before any MVC rule applies.
Confirm the failure is a CORS error
Before changing application settings, verify the failure mode in your browser developer tools. Open the Network tab and inspect the failed request to see whether an OPTIONS preflight call failed or the actual response arrived without the Access-Control-Allow-Origin header.
Browser consoles display a generic network error when a cross-origin check fails. Follow the diagnosis guide to confirm the failure is caused by CORS rather than an unhandled server exception or a transport issue.
Configure CORS at the controller or global MVC level
Spring MVC offers two configuration mechanisms: controller-level annotations and global configuration beans. The first level uses @CrossOrigin on a @RestController class or a specific handler method, such as @CrossOrigin(origins = "https://app.example.com"). A bare @CrossOrigin without attributes allows all origins, all headers, and all HTTP methods the handler is mapped to, with credentials disabled and a 30-minute maxAge.
@RestController
@RequestMapping("/api/items")
public class ItemController {
// Only this origin may read responses from a browser
@CrossOrigin(origins = "https://app.example.com")
@GetMapping("/{id}")
public Item getItem(@PathVariable long id) {
// ...
}
}The second level configures cross-origin rules globally per path pattern with a @Configuration class that implements WebMvcConfigurer and overrides addCorsMappings(CorsRegistry). When only addMapping is called without further options, the global configuration permits all origins, all headers, and the GET, HEAD, and POST methods.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}Restrict origins for production
A wildcard origin allows any external site to make browser requests against your API. For an API that reads or modifies private data, that is a serious exposure. Production configuration should list explicit https origins, loaded from environment-specific configuration values rather than hardcoded.
Global MVC configuration and controller-level @CrossOrigin annotations combine additively for list-based attributes such as origins, headers, and methods. For single-value attributes such as allowCredentials and maxAge, the local annotation value overrides the global value.
Handle preflights when Spring Security is present
When Spring Security is on the classpath, its filter chain executes before Spring MVC. A preflight OPTIONS request carries no cookies, so it has no JSESSIONID. If the security chain requires authentication, it rejects the preflight as unauthenticated with a 401 or 403, and the browser reports a CORS failure before Spring MVC ever evaluates @CrossOrigin or WebMvcConfigurer rules.
The fix is to enable CORS inside the security chain with http.cors(Customizer.withDefaults()) and declare a UrlBasedCorsConfigurationSource bean. If Spring MVC is on the classpath and no CorsConfigurationSource bean exists, Spring Security falls back to the MVC CORS configuration, but http.cors(...) must still be invoked in the SecurityFilterChain.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Without this, the chain rejects preflights before MVC CORS applies
http.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}Send cookies without hitting the wildcard trap
Browsers reject a response with Access-Control-Allow-Origin: * whenever the request carries credentials such as cookies. Spring enforces the same rule: allowCredentials(true) forbids allowedOrigins("*"). Configure explicit origins, or use allowedOriginPatterns("https://*.example.com") to match a dynamic set of subdomains.
The client must also opt in by setting credentials: 'include' on the fetch call. Because cookies and Authorization headers carry session secrets, never route credentialed requests through a public proxy.
Verify the headers with curl
A preflight probe sends the Origin and Access-Control-Request-Method headers and expects a 2xx status with access-control-allow-origin and access-control-allow-methods in the response. A plain GET probe sends only the Origin header and expects access-control-allow-origin on the actual response.
# Preflight probe: runs before the browser would send the PUT
curl -i -X OPTIONS http://localhost:8080/api/items/1 \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT"
# Expect a 2xx with access-control-allow-origin and
# access-control-allow-methods covering PUT
# Actual request probe
curl -i http://localhost:8080/api/items/1 \
-H "Origin: https://app.example.com"
# Expect access-control-allow-origin: https://app.example.comcurl does not enforce CORS, so these probes check what the server emits rather than what a browser would accept. See the Postman comparison for why direct clients bypass the browser checks.
Good questions.
Does adding @CrossOrigin to a controller fix CORS when Spring Security is installed?
No. The security filter chain executes before Spring MVC. Without http.cors(Customizer.withDefaults()) in the chain, unauthenticated OPTIONS preflights are rejected before the controller annotation is ever evaluated.
Why does the preflight return 401 or 403 with Spring Security?
Preflight OPTIONS requests carry no cookies, including JSESSIONID, and no Authorization header. If the security chain requires authentication and CORS is not enabled on it, the preflight is rejected as unauthenticated.
Can allowedOrigins("*") be combined with allowCredentials(true)?
No. Wildcard origins are forbidden when credentials are enabled. Declare explicit origin URLs, or use allowedOriginPatterns to match a set of subdomains.
When should the cors.dev proxy be used instead?
The cors.dev free tier serves browser GET and HEAD calls to third-party public APIs on any public host; managed access adds write methods and custom API headers. When you control the Spring Boot backend, configure the headers on your own server.