ASP.NET Core / Basic: HTTP and Application Composition / Lesson 2
Middleware Order and Error Boundaries
Explain forward request flow, reverse response flow, short-circuiting, and response commitment before composing cross-cutting behavior.
On this page
Lesson goals
- Trace before/after behavior around next.
- Place error and endpoint-dependent logic deliberately.
- Distinguish short-circuiting from completing the whole business operation.
Middleware wraps the next request delegate
A middleware component can perform work before calling next, await the downstream delegate, and perform work afterward. Request-side execution follows registration order; after-work unwinds in reverse order when the awaited calls complete normally. A component that does not call next short-circuits the remaining pipeline. Ordering therefore changes behavior, not merely code style.
The first complete program builds an ApplicationBuilder chain in memory and records execution order. It uses the actual framework builder without starting a host or listener. The trace demonstrates delegate composition, while connection handling and real response commitment remain outside the test.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using var services = new ServiceCollection().BuildServiceProvider();
var builder = new ApplicationBuilder(services);
var trace = new List<string>();
builder.Use(next => async context => { trace.Add("outer-before"); await next(context); trace.Add("outer-after"); });
builder.Use(next => async context => { trace.Add("inner-before"); await next(context); trace.Add("inner-after"); });
builder.Run(context => { trace.Add("terminal"); return Task.CompletedTask; });
await builder.Build()(new DefaultHttpContext());
Console.WriteLine(string.Join(">", trace));
outer-before>inner-before>terminal>inner-after>outer-after
The terminal runs between before and reverse after phases; no HTTP listener is opened.
Components have prerequisites and observation scope
Exception handling belongs early enough to observe failures from the components it is responsible for. Logic requiring the selected endpoint must run after route matching, while authorization must precede executing protected endpoint behavior. Static-file middleware, CORS, authentication, authorization, and endpoint execution have documented ordering interactions. Do not copy an order list without knowing which components the application uses.
WebApplication can insert some middleware automatically when services are detected. Explicit order may still be needed when the default does not match the intended CORS/authentication or terminal behavior. Keep service registration separate from execution order and verify the actual application model for the selected framework version.
A terminal branch prevents later code from running
The second program uses a synthetic maintenance flag to stop before an application delegate. It reports 503 and verifies that the delegate did not run. This is a maintenance-behavior test, not an authentication mechanism. In a real pipeline, placement determines whether logging, security headers, and error handling still surround the short-circuited response.
A short-circuit can be intentional, such as cached output or a rejected request, but must preserve the response contract and required outer behavior. Do not place essential cleanup only in a downstream component that may never execute. Define what happens when the outer after-work itself throws.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
bool maintenance = true;
bool applicationRan = false;
RequestDelegate application = context => { applicationRan = true; return Task.CompletedTask; };
RequestDelegate gate = async context =>
{
if (maintenance) { context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; return; }
await application(context);
};
var context = new DefaultHttpContext();
await gate(context);
Console.WriteLine($"status={context.Response.StatusCode} applicationRan={applicationRan}");
status=503 applicationRan=False
The local flag returns 503 before the application delegate executes.
A committed response cannot be replaced retroactively
Once headers/body have started on the real server response, later middleware cannot freely replace status and headers as if nothing were sent. Check HasStarted when designing error translation and use OnStarting for suitable pre-commit work. Avoid writing arbitrary body content after next when the endpoint may have completed, streamed, or failed partway.
DefaultHttpContext with a memory body is useful but does not reproduce every server feature’s commit behavior. A test that assigns a header after writing to MemoryStream is not proof the same operation is valid on Kestrel. Use an appropriate isolated integration test for that claim and keep component assertions narrower.
An error boundary should preserve useful evidence
Catch where the application can translate, recover, or report a failure without corrupting state. Return a stable public error shape without stack traces, secrets, or internal SQL details. Keep diagnostic context in controlled logs with correlation identifiers. Do not turn every exception, including cancellation, into a generic 500 before identifying its meaning.
Test order, terminal branches, downstream failure, after-work failure, and the response-started case. The important assertions are which components ran and what response contract remained possible. A pipeline that merely avoids an unhandled exception may still send inconsistent status, duplicate output, or omit required audit behavior.
Common errors and fixes
After-work executes in the expected forward order.
- Why it happens
- The wrapping/unwinding model was misunderstood.
- How to fix it
- Trace awaited next calls and reverse completion order.
- Prevent it next time
- Keep a tiny order regression.
A protected endpoint runs before authorization.
- Why it happens
- Security behavior is placed after the action it should guard.
- How to fix it
- Place supported authorization before endpoint execution.
- Prevent it next time
- Test forbidden as well as allowed requests.
An error handler overwrites a started response.
- Why it happens
- Local assumptions ignored server commitment.
- How to fix it
- Design translation around response state and streaming behavior.
- Prevent it next time
- Include a real integration case when testing commitment.
Check your understanding
1. A request is short-circuited before a logging component. Why might the request disappear from that component’s records?
Show worked solution
The component is downstream and never executes. Move the required outer observation earlier or redesign logging around the full intended boundary, while controlling sensitive data. Verify both ordinary and short-circuit paths. Do not assume every registered middleware runs for every request; calling next determines reachability, and branch composition can further change it.
2. Why is a memory-stream test insufficient to approve changing status after body output?
Show worked solution
The memory feature may not implement the same transport/header-commit transitions as the real server. It proves local writes, not what was already sent to a client. Keep status/header selection before commitment and test server-dependent behavior with a supported isolated integration setup. A convenient fake must not redefine HTTP semantics for the application.
Key takeaways
- Middleware order controls both reachability and observation.
- Awaited next calls wrap downstream work and unwind in reverse.
- Server response commitment is a boundary a local fake may not reproduce.