Middleware order is not a suggestion. Put ForwardedHeaders after HTTPS redirection and your reverse proxy integration breaks. Put Authorization before Authentication and every request fails.
Common questions this answers
- What is the correct middleware order in ASP.NET Core?
- Why does ForwardedHeaders need to come first?
- What breaks if Authentication comes after Authorization?
- Where should OutputCache go relative to ResponseCompression?
- How do I know where to place custom middleware?
Definition (what this means in practice)
The ASP.NET Core middleware pipeline processes requests in the order middleware is registered. Each middleware can handle the request, pass it to the next middleware, or short-circuit the pipeline. The order determines which middleware sees what request state.
Think of middleware as layers of an onion: the first middleware added is the outermost layer (sees the request first, the response last). In practice, wrong ordering causes silent failures: HTTPS redirection loops, missing authentication, broken CORS, or incorrect client IPs from proxies.
Terms used
- Middleware: a component that handles HTTP requests and responses in a pipeline.
- Short-circuit: when middleware handles a request without calling the next middleware.
- Terminal middleware: middleware that does not call next (like static files for matched paths).
- Request delegate: the next middleware in the pipeline.
Reader contract
This article is for:
- Engineers configuring ASP.NET Core application pipelines.
- Teams debugging middleware ordering issues.
You will leave with:
- The canonical middleware order with rationale.
- Failure modes for each middleware when misordered.
- A complete Program.cs template.
This is not for:
- Writing custom middleware (implementation details).
- Deep-dives on individual middleware configuration.
Quick start (10 minutes)
If you need the correct order without explanation:
Verified on: ASP.NET Core (.NET 10).
var app = builder.Build();
// 1. Exception handling (catches errors from all subsequent middleware)
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// 2. Forwarded headers (before anything that uses scheme/host/IP)
app.UseForwardedHeaders();
// 3. HTTPS redirection (after forwarded headers know the real scheme)
app.UseHttpsRedirection();
// 4. Static files (short-circuits for static content)
app.UseStaticFiles();
// 5. Routing (makes route data available to subsequent middleware)
app.UseRouting();
// 6. CORS (after routing, before caching and auth)
app.UseCors();
// 7. Authentication (establishes identity)
app.UseAuthentication();
// 8. Authorization (checks permissions using identity)
app.UseAuthorization();
// 9. Output caching (caches authorized responses)
app.UseOutputCache();
// 10. Response compression (compresses before sending)
app.UseResponseCompression();
// 11. Rate limiting (after routing for endpoint-specific limits)
app.UseRateLimiter();
// 12. Endpoints
app.MapControllers();
app.Run();
The production middleware order
This is the order Microsoft recommends, with rationale for each position.
Request
|
v
[1] ExceptionHandler / DeveloperExceptionPage
[2] HSTS
[3] ForwardedHeaders
[4] HttpsRedirection
[5] Static Files
[6] Routing
[7] CORS
[8] Authentication
[9] Authorization
[10] Session
[11] OutputCache / ResponseCaching
[12] ResponseCompression
[13] Rate Limiting
[14] Custom Middleware
[15] Endpoint Mapping
|
v
Response (travels back up through middleware)
ForwardedHeaders: why it must come early
When your application runs behind a reverse proxy (Azure App Service, nginx, AWS ALB), the proxy terminates TLS and forwards requests over HTTP. Without ForwardedHeaders, your application sees:
- Scheme:
http(nothttps) - Host: internal proxy address
- Client IP: proxy IP (not real client)
What breaks if wrong
| ForwardedHeaders position | Failure |
|---|---|
| After HttpsRedirection | Infinite redirect loop (scheme appears as http) |
| After authentication | Auth cookies may use wrong secure flag |
| After rate limiting | Rate limits apply to proxy IP, not client IP |
| Missing entirely | HTTPS redirects loop; client IP is wrong |
Correct position
// After exception handling, before everything else
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
KnownNetworks = { /* your proxy networks */ }
});
app.UseHttpsRedirection(); // Now sees correct scheme
HTTPS Redirection and HSTS
HSTS tells browsers to always use HTTPS. HttpsRedirection redirects HTTP requests to HTTPS.
What breaks if wrong
| Issue | Failure |
|---|---|
| HSTS before ExceptionHandler | Errors may not set HSTS header |
| HttpsRedirection before ForwardedHeaders | Redirect loop behind proxy |
| HSTS in development | Localhost becomes HTTPS-only (annoying) |
Correct position
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts(); // Only in production
}
app.UseForwardedHeaders();
app.UseHttpsRedirection();
Static Files positioning
Static file middleware serves files from wwwroot and short-circuits the pipeline for matched files.
What breaks if wrong
| Position | Failure |
|---|---|
| After Routing | Unnecessary routing overhead for every static file |
| After Authentication | Static files require authentication (usually wrong) |
| After Authorization | Static files blocked by authorization policies |
Correct position
app.UseStaticFiles(); // Before routing - no auth, fast short-circuit
// Or use MapStaticAssets() after routing for fingerprinted assets
app.UseRouting();
app.MapStaticAssets(); // After routing, integrates with endpoint routing
Why UseAuthentication must come before UseAuthorization
This is the most common ordering mistake. Authentication establishes who the user is. Authorization checks what they can do.
What breaks if wrong
| Order | Failure |
|---|---|
| Authorization before Authentication | HttpContext.User is null; all authorization fails |
| Missing Authentication | User identity never populated |
| Missing Authorization | [Authorize] attributes ignored |
Correct position
app.UseAuthentication(); // First: establish identity
app.UseAuthorization(); // Second: check permissions
CORS positioning
CORS middleware adds headers for cross-origin requests. Position affects whether CORS headers appear on cached responses.
What breaks if wrong
| Position | Failure |
|---|---|
| After ResponseCaching | CORS headers may be cached incorrectly or missing |
| Before Routing | Cannot use endpoint-specific CORS policies |
| After Authorization | Preflight requests may be blocked |
Correct position
app.UseRouting();
app.UseCors(); // After routing, before caching
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCaching();
OutputCache and ResponseCompression order
These interact: do you cache compressed responses or compress cached responses?
Options
// Option A: Cache then compress (compress on every cache hit)
app.UseOutputCache();
app.UseResponseCompression();
// Option B: Compress then cache (cache compressed responses)
app.UseResponseCompression();
app.UseOutputCache();
Trade-offs
| Order | Effect |
|---|---|
| OutputCache then Compression | Cached uncompressed; compress on every response |
| Compression then OutputCache | Cached compressed; less CPU but more cache variants |
Most applications use Option A (OutputCache before ResponseCompression) for simplicity.
Rate Limiting positioning
Rate limiting can use global limits or endpoint-specific limits with attributes.
What breaks if wrong
| Position | Failure |
|---|---|
| Before Routing | Cannot use [EnableRateLimiting] attributes |
| Before ForwardedHeaders | Rate limits apply to proxy IP |
Correct position
app.UseRouting();
app.UseRateLimiter(); // After routing for endpoint-specific policies
Routing and endpoint mapping
UseRouting() makes route data available. Endpoint mapping (MapControllers, MapRazorPages) defines what routes exist.
What breaks if wrong
| Issue | Failure |
|---|---|
| UseRouting after middleware that needs routes | Route data unavailable |
| Missing UseRouting | Implicit routing may not work as expected |
| MapControllers before UseAuthorization | Authorization not applied |
Correct position
app.UseRouting(); // Makes route data available
// ... middleware that uses routing ...
app.UseAuthorization(); // Uses route data for policies
app.MapControllers(); // Defines endpoints
Custom middleware placement
Where should your custom middleware go?
Guidelines
| Middleware purpose | Position |
|---|---|
| Request logging | Early (after exception handler) |
| Security headers | After exception handler, before responses |
| Request transformation | Before routing |
| Response transformation | After endpoint middleware adds content |
| Metrics/timing | Early to capture full request |
Example: security headers
app.UseExceptionHandler("/Error");
app.UseHsts();
app.UseForwardedHeaders();
// Custom security headers - early, affects all responses
app.Use(async (context, next) =>
{
context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
await next();
});
app.UseHttpsRedirection();
Copy/paste artifact: complete production pipeline
var app = builder.Build();
// === Error Handling ===
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// === Forwarded Headers (proxy support) ===
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// === Security ===
app.UseHttpsRedirection();
// Security headers
app.Use(async (context, next) =>
{
context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
context.Response.Headers["X-Frame-Options"] = "DENY";
await next();
});
// === Routing ===
app.UseRouting();
// === Cross-Origin ===
app.UseCors("AllowSpecificOrigins");
// === Authentication & Authorization ===
app.UseAuthentication();
app.UseAuthorization();
// === Caching & Compression ===
app.UseOutputCache();
app.UseResponseCompression();
// === Rate Limiting ===
app.UseRateLimiter();
// === Static Assets ===
app.MapStaticAssets();
// === Endpoints ===
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Common failure modes
| Symptom | Likely cause |
|---|---|
| Infinite HTTPS redirect loop | ForwardedHeaders after HttpsRedirection |
| Authorization always fails | Authentication after Authorization |
| Client IP always same (proxy IP) | Missing or late ForwardedHeaders |
| CORS headers missing on cached responses | CORS after ResponseCaching |
| Rate limits ignore endpoint attributes | RateLimiter before Routing |
| Static files require login | StaticFiles after Authentication |
| Exceptions not caught | ExceptionHandler not first |
Checklist
- ExceptionHandler is first middleware (in production).
- ForwardedHeaders before HttpsRedirection (if behind proxy).
- Authentication before Authorization.
- Routing before middleware that needs route data.
- CORS before ResponseCaching.
- RateLimiter after Routing (for endpoint-specific limits).
- Static files before Authentication (unless auth required).
FAQ
Do I always need UseRouting()?
In .NET 6+, routing is added implicitly when you call Map* methods. Explicit UseRouting() is needed when middleware between routing and endpoints needs route data.
Can I have multiple exception handlers?
Yes, but only the first one catches exceptions. Subsequent ones only see handled responses.
Does middleware order affect performance?
Yes. Put short-circuiting middleware (static files) early to avoid unnecessary processing. Put expensive middleware late when possible.
What if I am not behind a proxy?
Skip ForwardedHeaders. It only matters when a reverse proxy terminates TLS or modifies headers.
How do I debug middleware order issues?
Add logging middleware at different points to see what state exists at each stage. Check request scheme, host, and user identity.
Why must UseAuthentication come before UseAuthorization?
UseAuthentication populates HttpContext.User with the authenticated identity. UseAuthorization checks that identity against policies. If reversed, User is null and all authorization fails silently.
How do I fix an infinite redirect loop in ASP.NET Core?
Usually caused by ForwardedHeaders being after HttpsRedirection. Behind a reverse proxy, the app sees scheme as "http" and keeps redirecting. Move UseForwardedHeaders before UseHttpsRedirection.
What to do next
Review your Program.cs middleware order against the reference in this article. If you are behind a reverse proxy, verify ForwardedHeaders is configured correctly.
For more on authentication configuration, read Authentication Patterns: Cookie vs JWT vs OIDC.
If you want help debugging middleware ordering issues, reach out via Contact.
References
- ASP.NET Core Middleware
- Middleware order
- Write custom ASP.NET Core middleware
- Configure ASP.NET Core to work with proxy servers
- Routing in ASP.NET Core
- Overview of ASP.NET Core authentication
Author notes
Decisions:
- Present canonical order with failure modes. Rationale: understanding what breaks is more memorable than just the correct order.
- Include ForwardedHeaders prominently. Rationale: proxy integration issues are common and hard to diagnose.
- Show OutputCache before ResponseCompression. Rationale: simpler mental model; CPU cost is usually acceptable.
Observations:
- ForwardedHeaders issues often manifest as redirect loops in production but work locally.
- Authentication/Authorization order is the most common mistake in code reviews.
- Teams often add middleware without considering pipeline position.