Welcome back! In our previous lesson, we explored the fundamentals of cross-origin resource sharing (CORS) and learned why it's such an important security mechanism in modern web development. Now, we're ready to dive deeper into one of the most critical — and often misunderstood — aspects of CORS: preflight requests. These special HTTP requests act as a security checkpoint that browsers implement before allowing certain types of cross-origin requests to proceed. Without proper understanding and handling of these requests, you'll find that many of your cross-origin api calls will be mysteriously blocked, leading to frustrating debugging sessions.
In this lesson, you'll learn what triggers preflight requests, how they work under the hood, and most importantly, how to configure your ASP.NET Core rest api to handle them correctly. We'll explore multiple approaches — from using ASP.NET Core's built-in middleware to implementing custom solutions — and we'll test everything with real C# code to ensure you see exactly how these concepts work in practice.
By the time we're done, you'll be able to confidently implement preflight-handling that's both secure and efficient, avoiding the common pitfalls that trip up many developers. Let's get started! 🚀
Preflight requests are a special type of HTTP request that browsers send automatically as part of the CORS mechanism. They serve as a "permission check" before the browser sends your actual request. Understanding when and why these requests occur is crucial for building robust cross-origin applications.
A preflight request is triggered when your cross-origin request is not considered "simple" by browser security standards. But what makes a request "simple"? A request is simple only if it meets all of these conditions:
- It uses one of three specific methods:
GET,HEAD, orPOST - For
POSTrequests, the content type must be one of:application/x-www-form-urlencoded,multipart/form-data, ortext/plain - It doesn't include any custom headers beyond a small set of allowed headers like
AcceptorContent-Language
Here's where many developers get confused: modern APIs typically use application/json for POST requests, which immediately triggers a preflight check. Similarly, any request using PUT, DELETE, or PATCH methods — which are common in APIs — will also trigger a preflight. Custom headers like (used for authentication) or will trigger preflights too.
Let's look at a concrete scenario that demonstrates why proper preflight-handling is essential. Imagine you're building a task management application where your frontend runs on http://localhost:3000 and needs to update a task by sending a PUT request to your api at http://localhost:5000.
Your frontend code might look something like this:
Before this PUT request is sent, the browser automatically creates and sends an OPTIONS request to check permissions. The browser is essentially asking three critical questions:
First, it checks the origin: "Does this server accept requests from http://localhost:3000?" Second, it verifies the method: "Is PUT an allowed method for this endpoint?" Third, it validates the headers: "Are the Content-Type and Authorization headers acceptable?"
If your server doesn't respond correctly to this OPTIONS request with proper CORS headers, the browser will block the actual PUT request entirely. You'll see an error in your browser console like this:
ASP.NET Core provides excellent built-in support for handling preflight requests through its CORS middleware. Let's start by implementing a basic but robust configuration that handles the most common scenarios.
First, we'll configure the CORS policy in our Program.cs file. This is where we define which origins, methods, and headers are allowed:
Let's break down what each part of this configuration does:
The WithOrigins method specifies which domains are allowed to make cross-origin requests to your api. In development, this is typically your frontend's local development server. The WithMethods list includes OPTIONS explicitly, though ASP.NET Core will handle OPTIONS requests automatically when you use the CORS middleware.
The WithHeaders method defines which custom headers the browser is allowed to include in requests. This is crucial because the Authorization header (used for authentication tokens) would trigger a if not explicitly allowed.
To truly understand how preflight requests work, let's create a test that simulates what a browser does. We'll write a C# test that sends an OPTIONS request and verifies the response:
Important: This test verifies that your server sends the correct CORS response headers, but it does not fully replicate how a browser enforces CORS. C#'s
HttpClientdoes not enforce CORS policies — it will happily send any request and accept any response regardless of headers. In a real browser, the CORS check happens on the client side: the browser inspects the response headers and blocks the request if they don't match. To fully verify that your CORS configuration works end-to-end, you should also test with an actual frontend application running on a different origin in a real browser.
When you run this test, you should see output similar to this:
In real-world applications, different parts of your api often have different security requirements. For example, you might want public endpoints that allow read-only access from multiple origins, while authenticated endpoints require stricter controls. ASP.NET Core makes this easy with named CORS policies that you can apply selectively using MapGroup() and .RequireCors().
Let's create three different CORS policies for different types of endpoints:
Notice how each policy is tailored to its use case. The PublicPolicy doesn't allow credentials and only permits GET requests, making it suitable for public data. The AuthPolicy allows credentials and common CRUD operations for authenticated users. The AdminPolicy uses for maximum flexibility but restricts access to a specific admin origin and includes a custom header.
When you're developing and debugging CORS issues, visibility into what's happening with preflight requests is invaluable. Let's create a custom middleware that logs detailed information about every preflight request your server receives.
Here's the middleware implementation:
This middleware checks if the incoming request is an OPTIONS request (which indicates a preflight). If it is, it logs both the request details and the response headers that your CORS middleware will send back. The OnStarting callback is used to log response headers right before they're sent to the client.
⚠️ This section is for educational purposes only. Manual preflight-handling is not recommended for production applications. ASP.NET Core's built-in CORS middleware is more robust, less error-prone, and easier to maintain. The manual approach shown here is intended to help you understand what happens under the hood, which can be valuable when debugging CORS issues or when you encounter edge cases.
While ASP.NET Core's built-in CORS middleware handles preflight requests automatically and is the recommended approach, understanding how to handle them manually provides valuable insight into how the mechanism works. This knowledge can be helpful when debugging issues or when you need extremely customized behavior.
Here's a custom middleware that manually handles preflight requests:
This middleware does two important things. First, it intercepts OPTIONS requests and responds immediately with the appropriate CORS headers and a status code. The statement prevents the request from continuing down the , which is exactly what we want for .
Through experience, several preflight-handling mistakes emerge as particularly common and problematic. Understanding these pitfalls will save you hours of debugging time.
-
Ignoring OPTIONS requests: If your api doesn't respond properly to
OPTIONSrequests, all non-simple cross-origin requests will fail. This is why using ASP.NET Core's CORS middleware is so valuable — it handlesOPTIONSrequests automatically. -
Misunderstanding preflight caching: Setting an appropriate max age using
SetPreflightMaxAge()reduces preflight requests and improves performance, but setting it too long (hours or days) can cause problems if you need to update your CORS policies — clients will keep using their cached responses. -
Forgetting about credentials: If your api uses cookies, authentication headers, or any form of credentials, you must call
AllowCredentials()in your CORS policy. When using credentials, you cannot use a wildcard (*) for allowed origins — you must specify exact origins. -
Insufficient method allowance: Developers often forget to include all necessary HTTP methods in their
WithMethods()configuration. If your api usesPUT,DELETE, orPATCH, these must be explicitly allowed in your CORS policy. -
: When handling manually, the response must include all necessary headers (, , , and if needed, ) and should use the status code.
In this lesson, we've thoroughly explored preflight requests — one of the most critical aspects of CORS. We learned what preflight requests are, when browsers trigger them, and how to properly handle them using ASP.NET Core's built-in CORS middleware with Minimal APIs. We implemented everything from basic configurations using MapGroup() and .RequireCors() to route-specific policies, added diagnostic logging for easier debugging, and explored common mistakes to avoid. The key takeaway is this: preflight requests are not obstacles to avoid, but rather a crucial security feature that protects both your api and your users.
In the upcoming practice exercises, you'll apply everything you've learned about preflight requests. You'll configure different CORS policies, test them with real requests, and troubleshoot common issues. In our next lesson, we'll explore more advanced CORS scenarios, including handling multiple origins dynamically, implementing origin validation with custom logic, and managing CORS in complex microservice architectures. You're making excellent progress on your journey to mastering CORS in ASP.NET Core. Keep up the outstanding work! 🌟
