Stop Redirects. Start Speaking HTTP Correctly.
APIs live and die by predictability.
One of the most common production issues I see in ASP.NET Core APIs is authentication behaving like a web app, not an API. The symptom is subtle, but the impact is huge:
❌ APIs returning 302 redirects and HTML pages instead of HTTP status codes.
Let’s fix that properly.
The Root Cause: Cookie Authentication Is UI-First
ASP.NET Core’s cookie authentication was designed primarily for browser-based applications (MVC, Razor Pages).
By default, when an unauthenticated user hits a protected endpoint:
ASP.NET Core returns 302 Found
Redirects the client to /Account/Login
Sends back HTML
This is perfect for browsers.
But for APIs?
Mobile apps don’t follow redirects correctly
JavaScript clients receive HTML instead of JSON
API gateways misinterpret responses
Monitoring and observability suffer
What APIs Should Do Instead
APIs must follow HTTP semantics strictly.
Correct Behavior
| Scenario | Correct Status Code |
|---|
| Not authenticated | 401 Unauthorized |
| Authenticated but not allowed | 403 Forbidden |
| Token expired | 401 Unauthorized |
| Invalid credentials | 401 Unauthorized |
No redirects.
No HTML.
Just status codes.
Default Behavior (Problematic)
Here’s what happens without configuration:
GET /api/orders
302 Found
Location: /Account/Login
Content-Type: text/html
This breaks:
SPAs
Mobile apps
API consumers
Swagger UI in some cases
The Fix: API-Friendly Cookie Authentication
You can keep cookie authentication and make it API-correct by overriding redirect events.
Basic Fix: Return 401 Instead of Redirect
builder.Services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
What Changed?
| Before | After |
|---|
| 302 Redirect | 401 Unauthorized |
| HTML Login Page | Empty / JSON-friendly response |
| Browser-centric | API-centric |
Handling Access Denied (403)
Authentication ≠ Authorization.
You should also override access denied behavior:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
},
OnRedirectToAccessDenied = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
}
};
});
Now your API communicates intent clearly.
Adding JSON Error Responses (Recommended)
APIs should return machine-readable responses .
options.Events.OnRedirectToLogin = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
ctx.Response.ContentType = "application/json";
return ctx.Response.WriteAsync("""
{
"error": "unauthorized",
"message": "Authentication is required to access this resource."
}
""");
};
This improves:
Client error handling
Frontend UX
Debugging
Mixed Applications (Web + API)
If your app serves both MVC + APIs , apply logic selectively:
options.Events.OnRedirectToLogin = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
}
ctx.Response.Redirect(ctx.RedirectUri);
return Task.CompletedTask;
};
This keeps:
Browser UX intact
API behavior correct
Why This Matters in Real Systems
Problems This Fix Prevents
React apps receiving login HTML
Mobile apps crashing on auth failures
API gateways retrying redirects endlessly
Security tools misclassifying responses
Benefits
✔ Predictable API contracts
✔ Correct REST semantics
✔ Cleaner client code
✔ Easier observability
✔ Fewer production surprises
HTTP Status Codes: (API-Focused)
Below is a practical reference for API developers.
🔵 1xx – Informational
| Code | Meaning |
|---|
| 100 | Continue |
| 101 | Switching Protocols |
| 102 | Processing (WebDAV) |
| 103 | Early Hints (NEW, HTTP/2+) |
🟢 2xx – Success
| Code | Meaning |
|---|
| 200 | OK |
| 201 | Created |
| 202 | Accepted |
| 204 | No Content |
| 206 | Partial Content |
🟡 3xx – Redirection (Avoid in APIs)
| Code | Meaning |
|---|
| 301 | Moved Permanently |
| 302 | Found |
| 303 | See Other |
| 304 | Not Modified |
| 307 | Temporary Redirect |
| 308 | Permanent Redirect (NEWER) |
🚨 APIs should rarely use these
🔴 4xx – Client Errors (Most Important for APIs)
| Code | Meaning |
|---|
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 405 | Method Not Allowed |
| 409 | Conflict |
| 410 | Gone |
| 415 | Unsupported Media Type |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
⚫ 5xx – Server Errors
| Code | Meaning |
|---|
| 500 | Internal Server Error |
| 501 | Not Implemented |
| 502 | Bad Gateway |
| 503 | Service Unavailable |
| 504 | Gateway Timeout |
Treat Auth as Part of Your API Contract
Authentication behavior is not an implementation detail .
It’s part of how clients interact with your system.
APIs should be boring, explicit, and predictable.
Returning a login page from an API violates that principle.
Summary
ASP.NET Core gives us excellent defaults—but defaults are optimized for web apps , not APIs .
If you expose APIs:
This small change prevents hours of debugging across teams.
Happy Coding!
I write about modern C#, .NET, and real-world development practices. Follow me on C# Corner for regular insights, tips, and deep dives.