CORS Configuration
Why the Browser Blocks Your Request
Your React dev server runs on http://localhost:5173 (Vite) and the API on http://localhost:8080. Different port means different origin. Browsers block JavaScript from reading responses from another origin unless the server allows it with CORS headers.
The error you see in the browser console:
Access to fetch at 'http://localhost:8080/api/products' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Note: CORS is enforced by the browser only. curl and Postman never complain, which is why "it works in Postman" is not proof.
Global CORS Config
package com.shopease.common.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Value("${shopease.frontend-url:http://localhost:5173}")
private String frontendUrl;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(frontendUrl) // never "*" together with credentials
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE")
.allowedHeaders("Content-Type", "Authorization")
.maxAge(3600); // cache the preflight for 1 hour
}
}For one controller only you can use @CrossOrigin(origins = "http://localhost:5173") on the class, but a single global config is easier to keep correct.
Tip: If you add Spring Security, CORS must also be enabled in the security filter chain with http.cors(Customizer.withDefaults()). See the Spring Security material.
The Preflight Request
For POST/PUT/DELETE with JSON, the browser first sends an OPTIONS "preflight" request to ask permission. Spring answers it from your CORS config:
Example
curl -i -X OPTIONS localhost:8080/api/products -H "Origin: http://localhost:5173" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type"Example output
HTTP/1.1 200 OK Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers Access-Control-Allow-Origin: http://localhost:5173 Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE Access-Control-Allow-Headers: content-type Access-Control-Max-Age: 3600
Example
The decision the server makes, in plain Java:
import java.util.*;
public class Main {
static final Set<String> ALLOWED_ORIGINS = Set.of("http://localhost:5173", "https://shopease.in");
static final List<String> ALLOWED_METHODS = List.of("GET", "POST", "PUT", "PATCH", "DELETE");
static String preflight(String origin, String method) {
if (!ALLOWED_ORIGINS.contains(origin)) return "403 Invalid CORS request (origin " + origin + ")";
if (!ALLOWED_METHODS.contains(method)) return "403 Invalid CORS request (method " + method + ")";
return "200 Access-Control-Allow-Origin: " + origin + ", Allow-Methods: " + String.join(",", ALLOWED_METHODS);
}
public static void main(String[] args) {
System.out.println(preflight("http://localhost:5173", "POST"));
System.out.println(preflight("https://shopease.in", "DELETE"));
System.out.println(preflight("https://evil.example", "GET"));
System.out.println(preflight("http://localhost:5173", "TRACE"));
}
}Output
200 Access-Control-Allow-Origin: http://localhost:5173, Allow-Methods: GET,POST,PUT,PATCH,DELETE 200 Access-Control-Allow-Origin: https://shopease.in, Allow-Methods: GET,POST,PUT,PATCH,DELETE 403 Invalid CORS request (origin https://evil.example) 403 Invalid CORS request (method TRACE)