Summary
A deployment change introduced a CORS misconfiguration that caused every browser request from the authorized Angular frontend to be rejected after moving the Spring Boot backend to a Dockerfile-based Railway deployment. The application stopped returning the required Access-Control-Allow-Origin header, causing all preflight checks to fail.
Root Cause
The failure was triggered by a combination of factors:
- Environment variable mismatch:
CLIENT_ALLOWEDwas not set, incorrectly set, or not passed into the Docker container on Railway. - Spring Boot 3 CORS behavior changes: Spring Security 6 requires explicit CORS enabling in the
SecurityFilterChain, not just aCorsConfigurationSourcebean. - Dockerfile build isolation: The environment inside the container differed from the Nixpacks environment, meaning previously working assumptions no longer held.
- Wildcard path registration mismatch:
"/**"registration works only when the CORS filter is actually invoked by Spring Security.
Key takeaway: The backend was running with no valid allowed origins, so Spring Security silently dropped all CORS headers.
Why This Happens in Real Systems
This pattern is extremely common when teams move from local builds to containerized deployments:
- Environment variables don’t propagate into containers unless explicitly configured.
- Security frameworks evolve, and defaults change between major versions.
- CORS is evaluated before authentication, so any misconfiguration blocks everything.
- Cloud platforms override or sanitize environment variables, especially when names or formats differ.
Real-World Impact
When CORS breaks in production:
- All browser clients fail, even though the backend is technically healthy.
- Monitoring shows no errors, because CORS is enforced client-side.
- API logs appear normal, since preflight requests are often ignored or not logged.
- Frontend teams think the backend is down, causing confusion and wasted debugging time.
Example or Code (if necessary and relevant)
A correct Spring Boot 3 CORS configuration typically looks like this:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(System.getenv("CLIENT_ALLOWED")));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
How Senior Engineers Fix It
Experienced engineers approach this systematically:
- Verify environment variables inside the running container (
docker exec envor Railway shell). - Log the resolved CORS configuration at startup to confirm the origin list.
- Explicitly enable
http.cors()in Spring Security 6. - Check for trailing slashes or protocol mismatches in allowed origins.
- Ensure Railway environment variables are mapped into the Docker runtime.
- Test preflight requests manually using
curl -H "Origin: ...".
They treat CORS as a configuration problem, not an authentication or networking issue.
Why Juniors Miss It
Less experienced developers often struggle because:
- They assume CORS is a backend bug, not a client-side browser enforcement.
- They trust that environment variables “just work” across deployment methods.
- They don’t realize Spring Boot 3/Security 6 no longer auto-enables CORS.
- They debug only the backend logs, missing that the failure happens in the browser.
- They overlook that Docker changes the runtime environment, even if the code is identical.
Bottom line: CORS failures are subtle, silent, and easy to misdiagnose unless you’ve been burned by them before.