This content originally appeared on Level Up Coding - Medium and was authored by Edgars Garsneks

Full article is also available for non-member readers by following this link as well as video version on YouTube.
You’re building a web app when suddenly you see an error: “Blocked by CORS policy”. If you’re as confused as most developers are when they first encounter this issue, don’t worry — you’re not alone.
Access to fetch at 'https://bank.com/api/balances'
from origin 'http://real-bank.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Let’s break down what CORS is and how to handle it.
What is CORS?
CORS stands for Cross-Origin Resource Sharing. It’s a security feature implemented by web browsers to protect users from malicious websites attempting to access resources from origins they are not permitted to.
By default, browsers block cross-origin requests unless the server explicitly allows them with CORS headers.
An origin is defined by the combination of the protocol (http or https), domain (example.com), and port (80, 443, etc.). For example, the origin of https://example.com:443 is different from http://example.com:80 or https://sub.example.com:443

Why it matters? Because a malicious site real-bank.com (a copy of your bank’s legitimate website) could try to make requests to your bank’s API (bank.com/api/) to transfer funds or steal sensitive details, without your knowledge. But because of different origins, CORS will block the request from real-bank.com to bank.com/api/ before the server even sees it.

Sounds great! Let’s just put our resources on the same origin and be done with it! However, this isn’t always possible or practical.
As your application grows, you may need to interact with third-party APIs or serve your frontend and backend from different origins. Even static assets like images or scripts may be hosted on different domains. In those cases, you’ll need to configure CORS on your server to allow these cross-origin requests.
Let’s work with this simple HTTP server that always returns a 200 OK response.
public static void main(String[] args) throws IOException {
var server = HttpServer.create(new InetSocketAddress(3000), 0);
server.createContext("/", exchange -> {
exchange.sendResponseHeaders(200, 0);
});
server.start();
}How does CORS work?
When a browser detects a cross-origin request, it first sends a preflight request (an OPTIONS request) to the target server to see if the actual request is safe to send. This preflight request includes the Origin header, which indicates the source of the request, as well as other headers that describe the request.

The server then must respond to the preflight request with the appropriate CORS headers to indicate whether the actual request is allowed or not.
The server must respond with CORS headers — most importantly, Access-Control-Allow-Origin, set either to the requesting origin or * (for all origins). If everything is set, then browser will send original request to the server.

But if the server does not include this header or have different origin, the browser will block the request by default.

This is why you might see errors in your browser’s console indicating that a request has been blocked by CORS policy. Because your server is not configured to allow requests from the origin of your web application.
Simple Requests vs. Preflight Requests
Not every cross-origin request requires a preflight. Browsers skip the preflight step if the request is considered simple.
A request is simple if it meets all of the following conditions:
- Uses one of the methods: GET, HEAD, or POST.
- Only includes “safe” headers such as Accept, Accept-Language, Content-Language, Content-Type (with values application/x-www-form-urlencoded, multipart/form-data, or text/plain).
- Doesn’t use custom headers, credentials, or non-standard content types.
Any request that doesn’t meet these criteria is a preflighted request. The browser will send an OPTIONS request first, asking the server for permission before sending the actual request.
Handling Preflight Request
While the preflight request is automatically sent by the browser, the decision to allow or deny the actual request is made by the server.
The primary header server needs to set is Access-Control-Allow-Origin. This header specifies which origins are permitted to access the resource. If this header is not set or does not match the origin of the requesting site, the browser will block the request.
Let’s set it to a wildcard (*) to allow all origins.
public static void main(String[] args) throws IOException {
var server = HttpServer.create(new InetSocketAddress(3000), 0);
server.createContext("/", exchange -> {
var responseHeaders = exchange.getResponseHeaders();
responseHeaders.add("Access-Control-Allow-Origin", "*");
exchange.sendResponseHeaders(200, 0);
});
server.start();
}That’s it! Now the browser and server can communicate successfully. But, we are not quite done yet.
Fine-Grained CORS
If you are not intending to serve the whole world, consider following least privilege principles. This can help prevent unauthorized access and potential security issues in the long run.
CORS provides different headers to fine-tune access control:
Access-Control-Allow-Origin
This header specifies which origins are allowed to access the resource. While using * is fine during development, in production you should specify the exact origin (e.g., https://bank.com) to limit access.
responseHeaders.add("Access-Control-Allow-Origin", "https://bank.com");Instead of having hardcoded origins, consider dynamically setting the Access-Control-Allow-Origin header based on the Origin request header. That is usually done by maintaining a list of allowed origins.
If the Origin header of the incoming request matches one of the allowed origins, you can set the Access-Control-Allow-Origin header to that origin.
var allowedOrigins = List.of("https://bank.com");
var requestHeaders = exchange.getRequestHeaders();
var responseHeaders = exchange.getResponseHeaders();
var origin = requestHeaders.getFirst("Origin");
if (origin != null && allowedOrigins.contains(origin)) {
responseHeaders.add("Access-Control-Allow-Origin", origin);
}Access-Control-Allow-Methods
This header lists the HTTP methods that are permitted when accessing the resource. You can specify multiple methods, separated by commas.
GET, POST, HEAD, are always enabled, but any other methods like PUT, DELETE will be rejected if not specified in the header.
responseHeaders.add("Access-Control-Allow-Methods", "PUT, DELETE");You don’t need to include OPTIONS in the list for preflight requests. Put it only if you intend to support ordinary OPTIONS requests.
Access-Control-Allow-Headers
This header specifies which headers can be sent to the server in the request. This is particularly important for custom headers that your application might use.
For example — including idempotency key — X-Request-Id in the request, so server could identify and deduplicate requests. If it is not on the allow list then browser will block the next request by CORS policy.
To allow custom header to be sent, just include it in the appropriate header.
responseHeaders.add("Access-Control-Allow-Headers", "X-Request-Id");Access-Control-Expose-Headers
This header specifies which headers can be read by the client from the response. By default, only a limited set of headers are exposed e.g. Content-Type, Content-Length, etc.
Any other custom headers that your application might use should be included here as well. Otherwise, when processing response with JavaScript, you will not be able to find the header, because it will be erased by browser.
For example — X-RateLimit-Remaining can be included to allow the client to read the remaining rate limit.
responseHeaders.add("Access-Control-Expose-Headers", "X-RateLimit-Remaining");Access-Control-Max-Age
This header specifies how long the results of a preflight request can be cached. This can help reduce the number of preflight requests made by the browser.
You could imagine sending a preflight call for each service call, could be costly in the long run, given that most cloud providers bill by the network traffic.
Again to allow caching of CORS you can specify that in the response headers with duration in seconds.
responseHeaders.add("Access-Control-Max-Age", "3600");Access-Control-Allow-Credentials
This header indicates whether or not the browser should include credentials (such as cookies) in the request. By default, credentials are not included in cross-origin requests. If you want to allow credentials, you need to set this header to true.
responseHeaders.add("Access-Control-Allow-Credentials", "true");If this header is set to true, then Access-Control-Allow-Origin cannot be set to *, as we don’t want to allow credentials to be passed around different origins.
Combining it all together, a typical CORS configuration in Java might look like this:
var allowedOrigins = List.of("https://bank.com");
var requestHeaders = exchange.getRequestHeaders();
var responseHeaders = exchange.getResponseHeaders();
var origin = requestHeaders.getFirst("Origin");
if (origin != null && allowedOrigins.contains(origin)) {
responseHeaders.add("Access-Control-Allow-Origin", origin);
responseHeaders.add("Access-Control-Allow-Methods", "GET, POST, PUT");
responseHeaders.add("Access-Control-Allow-Headers", "X-Request-Id");
responseHeaders.add("Access-Control-Expose-Headers", "X-RateLimit-Remaining");
responseHeaders.add("Access-Control-Max-Age", "3600");
responseHeaders.add("Access-Control-Allow-Credentials", "true");
}This is a simplified example of the CORS configuration process in any web application. Of course, in real-world scenario different modern frameworks and libraries may provide built-in support for CORS, making it easier to configure and manage these settings.
For example - Express.js has a built-in middleware called cors.
const cors = require('cors');
app.use(cors({
origin: 'https://bank.com',
methods: ['GET', 'POST', 'PUT'],
allowedHeaders: ['X-Request-Id'],
exposedHeaders: ['X-RateLimit-Remaining'],
maxAge: 3600,
credentials: true
}));Spring Boot has different ways to configure CORS, one of which is using the CrossOrigin annotation.
@RestController
@RequestMapping("/api")
@CrossOrigin(
origins = "https://bank.com",
allowedHeaders = "X-Request-Id",
exposedHeaders = "X-RateLimit-Remaining",
maxAge = 3600,
allowCredentials = true
)
public class ApiController {
Or a configuration class:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://bank.com")
.allowedMethods("GET", "POST", "PUT")
.allowedHeaders("X-Request-Id")
.exposedHeaders("X-RateLimit-Remaining")
.maxAge(3600)
.allowCredentials(true);
}
}
But at the end it boils down to the same fundamental principles outlined above.
Closing Thoughts
Remember: CORS is a browser-enforced security feature. The server declares who’s allowed, but only the browser enforces it.
CORS isn’t about protecting your API from attackers — it’s about protecting users in their browsers. Malicious actors with tools like Postman can still call your API directly. That’s why you need additional layers of security: authentication, rate limiting, and proper authorization.
CORS is just one piece of the puzzle, but an important one.
If you like what you see, support author Edgars Garsneks, by subscribing so you don’t miss the next one!
CORS Explained: Stop Struggling with Cross-Origin Errors was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Edgars Garsneks
Edgars Garsneks | Sciencx (2025-09-03T15:05:47+00:00) CORS Explained: Stop Struggling with Cross-Origin Errors. Retrieved from https://www.scien.cx/2025/09/03/cors-explained-stop-struggling-with-cross-origin-errors/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.