Handling Preflight Requests

Introduction

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! 🚀

What Preflight Requests Are And When They Occur

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, or POST
  • For POST requests, the content type must be one of: application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • It doesn't include any custom headers beyond a small set of allowed headers like Accept or Content-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 RESTful APIs — will also trigger a preflight. Custom headers like Authorization (used for authentication) or X-Api-Key will trigger preflights too.

When a preflight is triggered, here's what happens behind the scenes:

The browser first sends an OPTIONS request to the server. This request asks, "Is it okay if I send a request from this origin, using this method, with these headers?" The server must respond with specific CORS headers that indicate what's allowed. Only if the server says "yes, that's all fine" will the browser proceed to send your actual request with the data you intended.

This might seem like extra overhead, but it's a critical security feature. Without preflights, a malicious website could potentially send dangerous requests to your api on behalf of authenticated users without their knowledge.

Why Understanding Preflights Matters For Your API

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:

JavaScript
fetch('http://localhost:5000/api/tasks/123', {
    method: 'PUT',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token-here'
    },
    body: JSON.stringify({ title: 'Updated Task', completed: true })
});

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:

text
Access to fetch at 'http://localhost:5000/api/tasks/123' from origin 'http://localhost:3000' 
has been blocked by CORS policy: Response to preflight request doesn't pass access control check: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

This error is frustrating because your api endpoint might be working perfectly — the problem is that it's not properly configured to handle the preflight request that comes before your actual request. The solution lies in configuring your ASP.NET Core application to respond appropriately to these OPTIONS requests.

Implementing Basic Preflight Configuration

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:

C#
var builder = WebApplication.CreateBuilder(args);

// Configure CORS with preflight support
builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAppPolicy", policy =>
    {
        policy.WithOrigins("http://localhost:3000")
              .WithMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
              .WithHeaders("Content-Type", "Authorization", "X-Requested-With")
              .AllowCredentials()
              .SetPreflightMaxAge(TimeSpan.FromMinutes(10));
    });
});

var app = builder.Build();

app.UseCors("MyAppPolicy");

// Define API endpoints
var api = app.MapGroup("/api/tasks").RequireCors("MyAppPolicy");

api.MapGet("/", () => Results.Ok(new { message = "Tasks retrieved successfully" }));

api.MapPut("/{id}", (int id, TaskUpdateDto taskData) => 
    Results.Ok(new { message = $"Task {id} updated successfully", data = taskData }));

app.Run();

public record TaskUpdateDto(string Title, bool Completed);

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 preflight if not explicitly allowed.

The AllowCredentials call is important when your api uses cookies or authentication. It tells the browser that your server will accept credentials in cross-origin requests. Note that when using AllowCredentials(), you cannot use a wildcard (*) for origins — you must specify exact origins.

Finally, SetPreflightMaxAge tells browsers how long (in seconds) they can cache the preflight response. This reduces the number of preflight requests by letting the browser remember that a particular request pattern is allowed. Ten minutes is a reasonable balance between performance and flexibility.

Notice how we use MapGroup() to create a route group for our task endpoints, and then chain .RequireCors("MyAppPolicy") to apply the CORS policy to all endpoints in that group. This is the Minimal API approach to applying CORS policies to specific routes.

With this configuration in place, when a preflight request arrives, the CORS middleware automatically responds with these headers:

text
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS
Access-Control-Allow-Headers: Content-Type,Authorization,X-Requested-With
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600

These headers tell the browser exactly what's allowed, and if everything matches the requirements of the actual request, the browser will proceed with sending it. It's worth noting that different browsers may handle the Max-Age header differently — Safari, for instance, has been known to cache preflight responses for shorter periods than specified, resulting in more frequent preflight checks than you might expect.

Testing Your Preflight Configuration

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:

C#
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;

namespace MyApi.Tests;

public class PreflightTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public PreflightTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task Preflight_Request_Returns_Correct_CORS_Headers()
    {
        // Arrange - Create an OPTIONS request simulating browser preflight
        var request = new HttpRequestMessage(HttpMethod.Options, "/api/tasks/123");
        request.Headers.Add("Origin", "http://localhost:3000");
        request.Headers.Add("Access-Control-Request-Method", "PUT");
        request.Headers.Add("Access-Control-Request-Headers", "content-type,authorization");

        // Act - Send the preflight request
        var response = await _client.SendAsync(request);

        // Assert - Verify the CORS headers in the response
        Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
        Assert.Equal("http://localhost:3000", 
            response.Headers.GetValues("Access-Control-Allow-Origin").First());
        
        var allowedMethods = response.Headers.GetValues("Access-Control-Allow-Methods").First();
        Assert.Contains("PUT", allowedMethods);
        
        var allowedHeaders = response.Headers.GetValues("Access-Control-Allow-Headers").First();
        Assert.Contains("Authorization", allowedHeaders);
    }
}

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 HttpClient does 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:

text
Test Passed: Preflight_Request_Returns_Correct_CORS_Headers
Duration: 245ms

Response Status: 204 No Content
Response Headers:
  Access-Control-Allow-Origin: http://localhost:3000
  Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS
  Access-Control-Allow-Headers: Content-Type,Authorization,X-Requested-With
  Access-Control-Allow-Credentials: true
  Access-Control-Max-Age: 600

This test demonstrates that your server is correctly responding to preflight requests. The 204 No Content status code is the standard response for successful OPTIONS requests — it indicates that the server has processed the request successfully but doesn't have any content to return.

Configuring Route-Specific Policies

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:

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    // Public API policy - read-only access from multiple origins
    options.AddPolicy("PublicPolicy", policy =>
    {
        policy.WithOrigins("http://localhost:3000", "https://public-app.example.com")
              .WithMethods("GET", "OPTIONS")
              .WithHeaders("Content-Type")
              .SetPreflightMaxAge(TimeSpan.FromMinutes(10));
    });

    // Authenticated API policy - full access from main app only
    options.AddPolicy("AuthPolicy", policy =>
    {
        policy.WithOrigins("http://localhost:3000")
              .WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
              .WithHeaders("Content-Type", "Authorization")
              .SetPreflightMaxAge(TimeSpan.FromMinutes(10))
              .AllowCredentials();
    });

    // Admin API policy - full access from admin panel with all methods
    options.AddPolicy("AdminPolicy", policy =>
    {
        policy.WithOrigins("https://admin.example.com")
              .AllowAnyMethod()
              .WithHeaders("Content-Type", "Authorization", "X-Admin-Key")
              .SetPreflightMaxAge(TimeSpan.FromMinutes(5))
              .AllowCredentials();
    });
});

var app = builder.Build();

app.UseCors();

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 AllowAnyMethod() for maximum flexibility but restricts access to a specific admin origin and includes a custom header.

Now we can apply these policies to specific route groups using MapGroup() with .RequireCors():

C#
// Public endpoints - read-only access from multiple origins
var publicApi = app.MapGroup("/api/public").RequireCors("PublicPolicy");

publicApi.MapGet("/articles", () => 
    Results.Ok(new { articles = new[] { "Article 1", "Article 2" } }));

publicApi.MapGet("/articles/{id}", (int id) => 
    Results.Ok(new { id, title = $"Article {id}", content = "Content here" }));

// Authenticated user endpoints
var authApi = app.MapGroup("/api/auth").RequireCors("AuthPolicy");

authApi.MapGet("/profile", () => 
    Results.Ok(new { username = "johndoe", email = "john@example.com" }));

authApi.MapPut("/profile", (ProfileUpdateDto profile) => 
    Results.Ok(new { message = "Profile updated", data = profile }));

authApi.MapPost("/tasks", (TaskCreateDto task) => 
    Results.Created($"/api/auth/tasks/1", new { id = 1, title = task.Title }));

authApi.MapDelete("/tasks/{id}", (int id) => 
    Results.Ok(new { message = $"Task {id} deleted" }));

// Admin endpoints
var adminApi = app.MapGroup("/api/admin").RequireCors("AdminPolicy");

adminApi.MapGet("/users", () => 
    Results.Ok(new { users = new[] { "user1", "user2" } }));

adminApi.MapPatch("/settings/{id}", (int id) => 
    Results.Ok(new { message = $"Settings {id} partially updated" }));

adminApi.MapDelete("/users/{id}", (int id) => 
    Results.Ok(new { message = $"User {id} deleted" }));

app.Run();

public record ProfileUpdateDto(string Username, string Email);
public record TaskCreateDto(string Title, bool Completed);

With this configuration, GET requests from http://localhost:3000 or https://public-app.example.com will work fine for public endpoints, but any POST, PUT, or DELETE requests to those endpoints will be rejected during the preflight check. Meanwhile, the authenticated endpoints allow full CRUD operations but only from http://localhost:3000, and the admin endpoints are restricted to https://admin.example.com. This gives you fine-grained control over who can do what with your api.

You can also apply CORS policies to individual endpoints when needed:

C#
// Apply a specific policy to a single endpoint
app.MapGet("/api/special", () => Results.Ok(new { data = "special" }))
   .RequireCors("AuthPolicy");

Adding Diagnostic Logging

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:

C#
namespace MyApi.Middleware;

public class PreflightLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<PreflightLoggingMiddleware> _logger;

    public PreflightLoggingMiddleware(
        RequestDelegate next, 
        ILogger<PreflightLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Method == "OPTIONS")
        {
            _logger.LogInformation(
                "[PREFLIGHT] {Timestamp} - {Method} {Path}",
                DateTime.UtcNow.ToString("o"),
                context.Request.Method,
                context.Request.Path);

            _logger.LogInformation(
                "  Origin: {Origin}",
                context.Request.Headers["Origin"].ToString());

            _logger.LogInformation(
                "  Requested Method: {Method}",
                context.Request.Headers["Access-Control-Request-Method"].ToString());

            _logger.LogInformation(
                "  Requested Headers: {Headers}",
                context.Request.Headers["Access-Control-Request-Headers"].ToString());

            context.Response.OnStarting(() =>
            {
                _logger.LogInformation("  Response CORS headers:");
                _logger.LogInformation(
                    "    Allow-Origin: {Origin}",
                    context.Response.Headers["Access-Control-Allow-Origin"].ToString());
                _logger.LogInformation(
                    "    Allow-Methods: {Methods}",
                    context.Response.Headers["Access-Control-Allow-Methods"].ToString());
                _logger.LogInformation(
                    "    Allow-Headers: {Headers}",
                    context.Response.Headers["Access-Control-Allow-Headers"].ToString());
                return Task.CompletedTask;
            });
        }

        await _next(context);
    }
}

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.

To use this middleware, register it in your Program.cs before the CORS middleware:

C#
var app = builder.Build();

app.UseMiddleware<PreflightLoggingMiddleware>();
app.UseCors();

// Define your route groups and endpoints
var api = app.MapGroup("/api/tasks").RequireCors("MyAppPolicy");
api.MapGet("/", () => Results.Ok(new { tasks = new[] { "Task 1", "Task 2" } }));
api.MapPut("/{id}", (int id, TaskUpdateDto task) => 
    Results.Ok(new { message = $"Task {id} updated" }));

app.Run();

The order matters here. By placing the logging middleware before the CORS middleware, we ensure that we log the request before CORS processing happens, and we can capture the CORS response headers as they're being set.

When a preflight request comes in, you'll see output like this in your logs:

text
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      [PREFLIGHT] 2024-01-15T14:35:22.657Z - OPTIONS /api/tasks/123
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      Origin: http://localhost:3000
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      Requested Method: PUT
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      Requested Headers: content-type,authorization
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      Response CORS headers:
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      Allow-Origin: http://localhost:3000
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      Allow-Methods: GET,POST,PUT,DELETE,PATCH,OPTIONS
info: MyApi.Middleware.PreflightLoggingMiddleware[0]
      Allow-Headers: Content-Type,Authorization,X-Requested-With

This detailed logging helps you understand exactly what's being requested and how your server is responding, making it much easier to diagnose CORS issues.

Understanding Manual Preflight Handling

⚠️ 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:

C#
namespace MyApi.Middleware;

public class ManualCorsMiddleware
{
    private readonly RequestDelegate _next;

    public ManualCorsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Method == "OPTIONS")
        {
            context.Response.Headers.Append(
                "Access-Control-Allow-Origin", 
                "http://localhost:3000");
            context.Response.Headers.Append(
                "Access-Control-Allow-Methods", 
                "GET,PUT,POST,DELETE,OPTIONS");
            context.Response.Headers.Append(
                "Access-Control-Allow-Headers", 
                "Content-Type,Authorization");
            context.Response.Headers.Append(
                "Access-Control-Max-Age", 
                "600");
            context.Response.Headers.Append(
                "Access-Control-Allow-Credentials", 
                "true");

            context.Response.StatusCode = StatusCodes.Status204NoContent;
            return;
        }

        // Add CORS headers to actual requests
        context.Response.Headers.Append(
            "Access-Control-Allow-Origin", 
            "http://localhost:3000");
        context.Response.Headers.Append(
            "Access-Control-Allow-Credentials", 
            "true");

        await _next(context);
    }
}

This middleware does two important things. First, it intercepts OPTIONS requests and responds immediately with the appropriate CORS headers and a 204 No Content status code. The return statement prevents the request from continuing down the middleware pipeline, which is exactly what we want for preflight requests.

Second, for all other requests (the actual requests that follow successful preflights), it adds the Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers. This is crucial because these headers must be present on the actual response, not just the preflight response.

You can also handle OPTIONS requests explicitly using Minimal API endpoint routing:

C#
var app = builder.Build();

// Add CORS headers to all responses via middleware
app.Use(async (context, next) =>
{
    context.Response.Headers.Append(
        "Access-Control-Allow-Origin", 
        "http://localhost:3000");
    context.Response.Headers.Append(
        "Access-Control-Allow-Credentials", 
        "true");
    await next();
});

// Handle OPTIONS requests explicitly for all API routes
// The {**catch-all} is a catch-all route parameter that matches all remaining
// path segments, so "/api/{**catch-all}" will match any route starting with
// "/api/" (e.g., /api/tasks, /api/tasks/123, /api/users/5/profile, etc.)
app.MapMethods("/api/{**catch-all}", new[] { "OPTIONS" }, (HttpContext context) =>
{
    context.Response.Headers.Append(
        "Access-Control-Allow-Methods", 
        "GET,PUT,POST,DELETE,OPTIONS");
    context.Response.Headers.Append(
        "Access-Control-Allow-Headers", 
        "Content-Type,Authorization");
    context.Response.Headers.Append(
        "Access-Control-Max-Age", 
        "600");
    return Results.NoContent();
});

// Define your actual API endpoints
var api = app.MapGroup("/api");

api.MapGet("/tasks", () => 
    Results.Ok(new { tasks = new[] { "Task 1", "Task 2" } }));

api.MapPut("/tasks/{id}", (int id, TaskUpdateDto task) => 
    Results.Ok(new { message = $"Task {id} updated", data = task }));

app.Run();

public record TaskUpdateDto(string Title, bool Completed);

This approach uses the {**catch-all} route parameter to match all possible paths under /api/, allowing you to handle OPTIONS requests for any endpoint. The first middleware ensures that all responses include the necessary CORS headers, while the explicit OPTIONS mapping handles preflight requests.

While these manual approaches work, they require more maintenance and are more error-prone than using the built-in CORS middleware. For production applications, you should almost always use ASP.NET Core's built-in CORS support with MapGroup() and .RequireCors() unless you have very specific requirements that can't be met otherwise.

Avoiding Common Preflight Mistakes

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 OPTIONS requests, all non-simple cross-origin requests will fail. This is why using ASP.NET Core's CORS middleware is so valuable — it handles OPTIONS requests 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 uses PUT, DELETE, or PATCH, these must be explicitly allowed in your CORS policy.

  • Responding incorrectly to manual OPTIONS requests: When handling preflight requests manually, the response must include all necessary headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and if needed, Access-Control-Allow-Credentials) and should use the 204 No Content status code.

  • Not testing with actual cross-origin requests: Tools like Postman or curl don't enforce CORS policies the way browsers do. Always test your CORS configuration with an actual frontend application running on a different origin to verify real-world behavior.

By avoiding these common mistakes and following the patterns we've covered in this lesson, you'll be able to implement robust preflight-handling that works reliably across all browsers and scenarios.

Conclusion & Next Steps

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! 🌟

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal