Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

CORS: requests from another site

Say the blog front end is written separately — in React or Vue — and during development it runs at http://localhost:5173, while the API runs at http://localhost:8080. The page calls fetch("http://localhost:8080/api/posts"), and the browser refuses to give it the response. This is not Spring's doing: it is a browser protection.

What an origin and CORS are

An origin is the scheme, domain and port together: http://localhost:5173 and http://localhost:8080 are different origins, even though the domain is the same. By default the browser does not let a page script read responses from another origin. Otherwise any site open in one of your tabs could read data from your mail or your bank.

CORS (Cross-Origin Resource Sharing) is the rule by which the server itself tells the browser: "this origin may read my responses". It does so with the Access-Control-Allow-Origin header.

What the refusal looks like

Without any setup the browser console (F12, the Console tab) shows:

Access to fetch at 'http://localhost:8080/api/posts' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

An important detail: the server received the request and answered 200. The browser does the blocking — it does not hand the response to the script because the header is missing. That is why curl and Postman work while the page does not: they are not browsers and do not check CORS.

@CrossOrigin on a controller

// src/main/java/com/example/blog/PostController.java
@RestController
@RequestMapping("/api/posts")
@CrossOrigin(origins = "http://localhost:5173")
public class PostController {
    // ...
}

Let's check it, imitating a browser with the Origin header:

curl -i -H "Origin: http://localhost:5173" http://localhost:8080/api/posts
HTTP/1.1 200
Vary: Origin
Access-Control-Allow-Origin: http://localhost:5173
Content-Type: application/json

The Access-Control-Allow-Origin header is there, so the browser will give the response to the page. For a request with Origin: http://evil.example Spring answers 403 with the text Invalid CORS request.

The preflight request

Before a POST with JSON, a PUT or a DELETE, the browser first asks for permission with an OPTIONS request, called a preflight:

curl -i -X OPTIONS http://localhost:8080/api/posts \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"
HTTP/1.1 200
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 1800

Only after such a response does the browser send the real POST. Max-Age: 1800 lets it skip asking again for half an hour.

One setting for the whole API

Putting @CrossOrigin on every controller is inconvenient. Remove the annotation and describe the rule once:

// src/main/java/com/example/blog/WebConfig.java
package com.example.blog;
 
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 WebConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:5173")
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}

Now the rule applies to every /api/** address, including the images from the previous lesson. PATCH is not on the list, so a preflight for it gets 403.

When Spring Security arrives

In chapter six we add Spring Security. Its filters sit in front of the controllers, and a preflight request carries neither a password nor a token, so it gets 401 before the CORS setting has a chance to work. To prevent this, enable CORS in the API chain:

// src/main/java/com/example/blog/SecurityConfig.java
http
        .securityMatcher("/api/**")
        .cors(Customizer.withDefaults())
        // the rest of the chain settings

cors(Customizer.withDefaults()) takes the rules from the same WebConfig and answers the preflight before the login check.

Common mistake

Allowing everything at once: allowedOrigins("*") together with allowCredentials(true) (needed for the browser to send cookies). The application starts, but every request to /api/**, even from curl, fails. With the IllegalArgumentException handler from the previous lesson it is a 400:

{"detail":"When allowCredentials is true, allowedOrigins cannot contain the special value \"*\" since that cannot be set on the \"Access-Control-Allow-Origin\" response header. To allow credentials to a set of origins, list them explicitly or consider using \"allowedOriginPatterns\" instead.","instance":"/api/posts","status":400,"title":"Bad Request"}

List the actual front-end addresses. A star without cookies is fine only for public data that anyone may read anyway.

Thanks for reading!

Donate to the site — we work hard for you.