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)