LEARN · DEBUGGING GUIDE

ASP.NET Core Middleware Order: The Pipeline Configuration Bug That Silently Breaks Everything

Middleware order in ASP.NET Core is not declarative — every call to Use/UseMiddleware appends to a linear pipeline. One wrong line and your app silently drops requests, skips auth, or returns blank pages. Here's how to catch it fast.

IntermediateDotnet8 min read

What this usually means

Middleware runs in the order it's registered. ASP.NET Core builds a pipeline where each piece can either process the request and pass it to the next, or short-circuit (e.g., return 401). If you put authentication after CORS, the CORS middleware may send a 200 before auth ever sees the request. If static files are before authorization, protected files become public. If exception handling isn't first, exceptions in earlier middleware bypass your global handler. The pipeline is just a sequence of async delegates — order is everything and there's no compiler warning when you get it wrong.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Open Program.cs or Startup.cs and list every `app.Use*`, `app.Map*`, and `app.Run*` call in registration order.
  • 2Compare your order against the official Microsoft recommended order: ExceptionHandler → HSTS → HttpsRedirection → StaticFiles → Routing → Authentication → Authorization → CORS → ResponseCompression → Endpoints.
  • 3Add a test middleware at position 1 that writes a simple text response and never calls `next` — if you see that text, the pipeline is intact. Then move it to see where it breaks.
  • 4For CORS issues: send a curl OPTIONS request with Origin header and check if the response includes Access-Control-Allow-Origin. If the header is missing, CORS middleware ran but something before it short-circuited.
  • 5For blank responses: enable ASP.NET Core logging at Debug level (set Logging:LogLevel:Microsoft.AspNetCore to Debug in appsettings) and inspect the trace logs for 'Request starting' and 'Request finished' pairs — a gap indicating where the request was aborted.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchProgram.cs or Startup.cs (the central pipeline configuration)
  • searchappsettings.Development.json (logging levels for Microsoft.AspNetCore pipeline trace)
  • searchNetwork tab in browser DevTools (compare response headers for working vs. broken endpoints)
  • searchApplication Insights / ELK / any structured log sink (search for middleware-specific log entries)
  • searchKestrel or IIS logs (look for 'Connection id' and 'Request finished' messages with unexpected status codes)
  • searchAny custom middleware class files — check if they call `next` conditionally or not at all
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningRegistering UseCors before UseRouting or UseAuthentication — CORS runs before auth and returns 200 for preflight, but actual requests are unauthenticated
  • warningPutting UseExceptionHandler after other middleware that throws — exceptions never reach the error handler
  • warningUsing app.UseStaticFiles before app.UseAuthorization — static files served without auth check
  • warningAdding custom middleware after UseEndpoints — endpoint middleware short-circuits, custom code never runs
  • warningPlacing UseResponseCaching after UseAuthentication — cached responses may bypass auth for subsequent requests
  • warningUsing UseHttpsRedirection before UseExceptionHandler — redirects hide error pages, making debugging confusing
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildRewrite the pipeline in the exact Microsoft-recommended order: ExceptionHandler → HSTS → HttpsRedirection → StaticFiles → Routing → Authentication → Authorization → CORS → ResponseCompression → Endpoints
  • buildIf you need custom middleware before routing (e.g., request logging), place it before UseRouting but after UseExceptionHandler
  • buildFor CORS with JWT auth: ensure UseCors is called after UseAuthentication and UseAuthorization, and that the CORS policy allows credentials
  • buildSplit middleware registration into clearly commented sections with headers like // ---- ERROR HANDLING ----, // ---- AUTH ---- to prevent future misordering
  • buildAdd an integration test that sends requests to each endpoint and asserts the response status and headers match expectations — run it after every middleware change
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun a battery of HTTP requests: anonymous GET to a protected endpoint expects 401; authenticated GET to same expects 200 with data; OPTIONS with Origin expects 200 with CORS headers
  • verifiedCause an exception in a controller (throw new Exception("test")) and verify your error page or JSON error response appears, not raw stack trace
  • verifiedCheck response headers for the presence of 'X-Correlation-ID' or other custom headers your middleware should add
  • verifiedReview pipeline trace logs: 'Request finished' status code should be what you expect for each test case
  • verifiedRun `dotnet run` and open the app in a browser — no blank pages, no unexpected redirects
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDo not assume the order in the code comments reflects the actual order — always read the actual Register lines
  • warningDo not trust that 'it worked in development' — dev URLs often skip HSTS, CORS policies differ, and errors show yellow pages instead of custom handlers
  • warningDo not add middleware with `app.UseWhen` without understanding the conditional predicate — it can skip execution entirely
  • warningDo not place app.UseEndpoints early — endpoints should be last because they are terminal middleware (they send the response)
  • warningDo not modify pipeline order without checking if any middleware depends on state set by a previous middleware (e.g., HttpContext.Items)
( 07 )War story

Midnight CORS chaos: how a one-line reorder broke our SaaS API for three hours

Senior Backend EngineerASP.NET Core 6, JWT Bearer, SignalR, Azure App Service, Application Insights

Timeline

  1. 23:15Deploy a routine patch that adds UseResponseCaching middleware to improve API latency
  2. 23:20PagerDuty alert: 50% of API requests returning 401 Unauthorized
  3. 23:25Check Application Insights — many requests show 401 before any controller code runs
  4. 23:35Notice CORS preflight requests now fail with 405 instead of 200
  5. 23:50Compare last known good deploy's Program.cs — the new UseResponseCaching line was placed before UseAuthentication
  6. 00:15Move UseResponseCaching to after UseAuthorization in the pipeline
  7. 00:20Deploy fix: all endpoints return correct status codes, CORS works again
  8. 00:30Postmortem: nobody reviewed the middleware order; caching middleware short-circuited before auth could run

It was a quiet Wednesday night. I pushed a small optimization: add response caching to our .NET 6 API. The change was a single line: `app.UseResponseCaching();` I placed it right after `app.UseRouting();` — seemed logical. I ran the unit tests, they passed. I merged the PR and triggered a deployment to staging. Everything looked fine on staging because our staging environment doesn't send CORS preflight requests (same-origin).

Twenty minutes later, production was on fire. Half of our mobile users were getting 401s on endpoints they were already authenticated for. Our mobile dev team was pinging me on Slack. I pulled up Application Insights: every request with a valid JWT token was returning 401 before it reached any controller. The response headers showed no `Access-Control-Allow-Origin`. I grabbed a coffee and opened the deploy diff.

The moment I saw `app.UseResponseCaching()` between `UseRouting` and `UseAuthentication`, I knew. The caching middleware was checking the cache, finding nothing, and then short-circuiting the pipeline for responses that looked like they could be cached — but it never called `next()`. So the authentication middleware never ran. The fix was moving that one line to after `UseAuthorization`. Three hours of pager hell because of a one-line misplacement. We added a pipeline order linter to our CI the next day.

Root cause

UseResponseCaching placed before UseAuthentication in the middleware pipeline — caching middleware short-circuited requests before auth could validate tokens

The fix

Moved UseResponseCaching to after UseAuthorization in Program.cs, following Microsoft's recommended middleware order

The lesson

Middleware order is not just convention — it's correctness. Always place middleware in the documented order and add integration tests that verify the full request lifecycle under auth and CORS scenarios.

( 08 )How the ASP.NET Core Pipeline Actually Works

Each `app.Use()` call adds a `Func<RequestDelegate, RequestDelegate>` to a list. At app start, the framework composes these into a single `RequestDelegate` by chaining them: the last registered middleware becomes the innermost delegate. When a request arrives, the first middleware runs, optionally calls `next`, and so on until a middleware sends a response or reaches the end.

The key insight: there is no 'after' — a middleware that calls `next` will resume after the inner middleware completes, but only if the inner middleware doesn't short-circuit. If any middleware sets `httpContext.Response.StatusCode` and does not call `next`, the pipeline ends there. Common short-circuiters: `UseAuthentication` (401 if no valid token), `UseAuthorization` (403), `UseCors` (200 for preflight), and any custom middleware that returns early.

( 10 )Debugging Pipeline Order with Logging and Middleware Inspection

Enable `Microsoft.AspNetCore` logging at Debug level: in `appsettings.Development.json`, set `"Microsoft.AspNetCore": "Debug"`. This emits trace-level logs showing each middleware invocation. Look for lines like `'AuthenticationMiddleware' started` and `'AuthenticationMiddleware' finished`. If you see a middleware start but not finish, it likely short-circuited.

You can also write a diagnostic middleware that logs the current pipeline position: `app.Use(async (context, next) => { Console.WriteLine($"Middleware before {context.Request.Path}"); await next(); Console.WriteLine($"Middleware after {context.Request.Path}"); });`. Place this at various points to observe the order of execution. Remember: the console output order tells you the pipeline structure.

( 11 )Integration Testing the Pipeline Order

The most reliable way to prevent order bugs is to write integration tests that verify the full request lifecycle. Use `Microsoft.AspNetCore.TestHost` to create a `TestServer` with your actual `Startup` or `WebApplication` configuration. Then send HTTP requests and assert on status codes and headers. Key test cases: an unauthenticated request to a protected endpoint returns 401; an authenticated request to a protected endpoint returns 200; a CORS preflight request returns 200 with correct headers; a request that triggers an exception returns your custom error response.

Run these tests as part of your CI pipeline. If someone reorders middleware, a test will fail. This catches the problem in seconds, not hours.

( 12 )Common Anti-Patterns and How to Spot Them in Code Review

Anti-pattern 1: 'I'll just put this middleware here because it's close to similar middleware.' — No. Always follow the documented order. Anti-pattern 2: 'The middleware doesn't call next, so order doesn't matter.' — Wrong. A non-calling middleware blocks everything after it. Anti-pattern 3: 'It works on my machine.' — The development environment likely has different CORS policies, no HTTPS redirection, and different error pages. Always test in a staging environment that mirrors production.

During code review, visually trace the pipeline from top to bottom. If you see a middleware that calls `next` conditionally (e.g., `if (condition) { await next(); }`), flag it — it's a potential short-circuit point. Also flag any middleware placed after `UseEndpoints`; endpoints are terminal and should be last.

Frequently asked questions

What is the exact middleware order for ASP.NET Core 6+?

The official recommended order for common middleware is: ExceptionHandler → HSTS → HttpsRedirection → StaticFiles → Routing → Authentication → Authorization → CORS → ResponseCompression → Endpoints. This is from Microsoft's docs. Place any custom middleware according to its dependency: error handling first, auth-related after routing, and terminal middleware last.

How do I check the current middleware order at runtime?

You can't easily enumerate the pipeline at runtime, but you can add a diagnostic middleware that logs its position. Alternatively, enable Microsoft.AspNetCore logging at Debug level and inspect the trace logs. The log output will show the sequence of middleware invocations for each request.

Can I use `app.UseWhen` to conditionally skip middleware? Does it affect order?

Yes, `UseWhen` adds a conditional branch in the pipeline. The branch is still in the same order relative to other middleware. However, if the predicate fails, the middleware inside the branch is skipped entirely. This can lead to surprising behavior if the predicate is misconfigured. Always test both branches.

Why does CORS work in development but break in production?

Often because development uses a proxy (e.g., with SPA) that handles CORS, or the dev URL is same-origin. Also, production often has HTTPS redirection middleware that runs before CORS, which might redirect OPTIONS requests. Ensure your CORS middleware is placed after redirection and before endpoints, and that the CORS policy is correctly configured for your production origins.

How do I fix a blank 200 response caused by middleware order?

Blank 200 usually means a middleware set the status code to 200 but didn't write any body. Common culprits: `UseCors` for preflight (returns 200 with no body, which is correct), or a custom middleware that short-circuits early. Check the response headers: if you see CORS headers but no body, the request was a preflight. If it's not a preflight, look for middleware that returns without calling `next`. Enable debug logging to see which middleware finishes early.