How an ASP.NET Core Request Actually Flows
Trace requests and responses through the middleware chain.
Predict three paths
Suppose wrappers A and B each log in, await the next delegate, then log out, and a final endpoint logs endpoint. Write your traces first, then compare the three outcomes:
- Normal path: A in, B in, endpoint, B out, A out.
- B returns immediately after
B in: A in, B in, A out. The endpoint and B's later code never run. - The endpoint throws and neither wrapper catches it: A in, B in, endpoint. Plain statements after the failed await are skipped. Put cleanup in
finallywhen it must run during unwinding.
The mental model: a chain, not a checklist
Each component can call the next delegate or stop. Entered wrappers resume in reverse order when that awaited call returns normally; an escaping exception skips plain after-await statements. A finally block has separate unwinding behavior. This is why a wrapper can inspect a downstream status without the endpoint having run.
| Illustrative request path | What it contributes | Can later work be skipped? |
|---|---|---|
| Exception handler → HSTS / HTTPS redirect | Handles eligible downstream failures; applies the deployment’s transport policy | A redirect can finish the request |
| Static files | Serves a matching public asset in this ordering | Yes; later routing/auth do not run for that served file |
| Routing → CORS | Selects endpoint metadata; applies cross-origin policy | CORS can answer an applicable preflight |
| Authentication → authorization | Establishes identity; evaluates access | A scheme-specific handler, challenge or forbid can end this path |
| Endpoint | Executes the selected handler | Plain after-await wrapper code resumes only after a normal return |
Use these two consequences when checking the actual configuration:
- A short-circuit skips later middleware for that request. When
UseStaticFilesserves a matching file here, later authorization does not run. A missing file normally continues downstream; do not store protected files in this public path. - Anything that wraps the rest must be registered first. The exception handler can only catch what runs after it.
Order that actually matters
This is a typical production ordering excerpt, not a universal template. Assume builder already registers controllers, authentication, authorization, and the named CORS policy "default"; the /error handler also exists. Keep HSTS out of local development.
var app = builder.Build();
app.UseExceptionHandler("/error"); // first: it must wrap everything below
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles(); // before routing: cheap exit for assets
app.UseRouting(); // selects the endpoint before endpoint-aware policies
app.UseCors("default"); // after routing, before auth
app.UseAuthentication(); // who are you?
app.UseAuthorization(); // are you allowed? needs the endpoint + the identity
app.MapControllers(); // registers controller endpoints for later execution
app.Run();Why UseRouting comes before endpoint-aware authorization: the selected endpoint supplies [Authorize] metadata. Do not describe the wrong order as a guaranteed silent bypass: with the normal security-metadata check enabled, endpoint execution throws InvalidOperationException when authorization metadata exists but authorization middleware did not run with that endpoint selected. Test the actual configuration.
WebApplication can add routing and authentication/authorization middleware automatically when applicable. Explicit calls make the intended ordering visible; reason about the resulting pipeline, not just the presence of method names.
Short-circuiting, concretely
A middleware that returns without calling next skips downstream execution; already-entered upstream middleware can still resume.
| Step in this limiter-first example | Reached? | Observation |
|---|---|---|
| An already-entered outer wrapper | Yes | It has not finished its await yet |
Limiter rejects GET /api/orders | Yes | Sets 429 and returns without calling next |
| Downstream authentication and endpoint | No | Limiter placement determines what is skipped here |
| Outer wrapper after the normal return | Yes | Can observe 429 unless a separate exception interrupts it |
In this illustrative fragment, IsOverLimit and Log are application-provided helpers. The decision is whether to call next:
app.Use(async (context, next) =>
{
if (IsOverLimit(context))
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
return; // short-circuit: nothing below runs
}
await next(context); // continue down the chain
// This runs after downstream processing returns normally (even if it short-circuited).
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
{
Log.Warning("Unauthorized {Path}", context.Request.Path);
}
});Three ordering bugs worth recognising
1. UseCors after UseAuthorization. A browser preflight does not carry the actual request's credentials. If authorization rejects it before CORS runs, the response can lack CORS headers. The same ordering can hide an unauthorized GET response from browser code too. Whether a particular OPTIONS request is rejected depends on routing and policy; CORS is not authentication.
2. Writing to the response after next. Once the response has started, headers are already on the wire:
await next(context); context.Response.Headers["X-Trace"] = traceId; // unsafe if the response has already started
Register context.Response.OnStarting(...) before calling next so its callback can set the header before headers are sent. Registering the callback after the response starts is too late.
3. Exception handler registered late. It cannot catch failures in earlier middleware. Test the resulting error response rather than assuming that every failure produces a connection reset.
How to verify it rather than trust it
Order is observable. Log on the way down and on the way back up, and the nesting becomes obvious:
app.Use(async (context, next) =>
{
Console.WriteLine($"-> {context.Request.Path}");
await next(context);
Console.WriteLine($"<- {context.Response.StatusCode}");
});For authorization, this xUnit test fragment assumes an existing application factory, a protected /api/orders route, and an authentication scheme configured to challenge with HTTP 401:
[Fact]
public async Task Protected_endpoint_rejects_anonymous_caller()
{
var client = factory.CreateClient();
var response = await client.GetAsync("/api/orders");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}Also test an allowed identity, an authenticated-but-forbidden identity, and cross-origin preflight. A single anonymous-response assertion does not prove every policy or branch is configured correctly.
Questions you should be able to answer
- Why must
UseRoutingprecedeUseAuthorization? - What happens to a request for
/logo.pngwhenUseStaticFilessits aboveUseAuthentication? - Where would you add a header that must appear on every response, including error responses?
- A middleware logs a 401 that your controller never returned. Where did it come from?
- Can placing
UseCorsbelowUseAuthorizationaffect both preflight and ordinary GET responses?
Answer key
- Authorization needs the selected endpoint's policy metadata. The framework's endpoint guard can detect missing correctly positioned authorization; it is not safe to infer a silent bypass.
- If the file is served, downstream authentication and authorization are skipped. If it is not served, the request continues.
- Register an
OnStartingcallback in middleware reached before the relevant branches and before response start. No application callback can cover responses generated entirely outside that pipeline. - A downstream authorization challenge or another short-circuiting component may have produced it. Inspect the selected endpoint, authentication scheme, and logs; the controller need not have executed.
- Yes. Early authorization rejection can prevent CORS headers on either response. A simple GET avoids preflight only when it meets the browser's simple-request conditions; it still needs appropriate CORS response headers.
TechInterviewNotes Editorial Team · Updated 28 Oct 2026 ·