Skip to main content

aspnetcore-middleware-headers

How to safely set response headers in ASP.NET Core middleware

Source facts

Repository
bradygaster/squad-places
Last source activity
March 6, 2026 at 16:16
Detected SKILL.md language
English
Stars
8
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
aspnetcore-middleware-headers
description
How to safely set response headers in ASP.NET Core middleware
domain
aspnetcore, middleware, http
confidence
high
source
earned (bug fix 2026-02-24)
## Context When writing custom ASP.NET Core middleware that needs to set response headers, the timing of when you set headers matters. Kestrel begins streaming response bodies once the initial buffer (~16KB) is filled, at which point response headers become read-only. ## Patterns ### ✅ Correct: Use `OnStarting` callback Set response headers using the `context.Response.OnStarting()` callback **before** calling `await next()`: ```csharp app.Use(async (context, next) => { // Register header-setting logic before response starts context.Response.OnStarting(() => { if (!context.Response.Headers.ContainsKey("X-Custom-Header")) { context.Response.Headers["X-Custom-Header"] = "value"; } return Task.CompletedTask; }); await next(); }); ``` **Why this works:** The `OnStarting` callback is guaranteed to execute before the first byte of the response body is written, regardless of response size or buffering behavior. ### ✅ Alternative: Check `HasStarted` flag If you must set headers after `next()`, check `Response.HasStarted` first: ```csharp app.Use(async (context, next) => { await next(); if (!context.Response.HasStarted && !context.Response.Headers.ContainsKey("X-Custom-Header")) { context.Response.Headers["X-Custom-Header"] = "value"; } }); ``` **Caveat:** This approach means headers won't be present on responses that started streaming early. Use `OnStarting` if you need guaranteed header presence. ## Anti-Patterns ### ❌ Setting headers after `next()` without checking ```csharp app.Use(async (context, next) => { await next(); // ❌ CRASHES if response body > initial buffer size context.Response.Headers["X-Custom-Header"] = "value"; }); ``` **Why this fails:** - Kestrel starts streaming once the response buffer is full (~16KB) - Once streaming starts, `Response.HasStarted` is `true` and headers are read-only - Attempting to set headers throws `InvalidOperationException: Headers are read-only, response has already started` **Symptoms:** - Small responses work (fit in buffer) - Large responses crash (OpenAPI docs, HTML pages, large JSON) - Intermittent failures depending on response size ## Examples Real-world example from SquadPlaces (2026-02-24): **Before (broken):** ```csharp app.Use(async (context, next) => { var blocklist = context.RequestServices.GetRequiredService<IpBlocklistService>(); var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; if (blocklist.IsBlocked(ip)) { context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsJsonAsync(new { error = "Blocked" }); return; } await next(); // ❌ Crashes on large responses if (!context.Response.Headers.ContainsKey("X-RateLimit-Limit")) { var isWrite = HttpMethods.IsPost(context.Request.Method); context.Response.Headers["X-RateLimit-Limit"] = isWrite ? "30" : "60"; } }); ``` **After (fixed):** ```csharp app.Use(async (context, next) => { var blocklist = context.RequestServices.GetRequiredService<IpBlocklistService>(); var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; if (blocklist.IsBlocked(ip)) { context.Response.StatusCode = StatusCodes.Status403Forbidden; await context.Response.WriteAsJsonAsync(new { error = "Blocked" }); return; } // ✅ Headers set before response starts context.Response.OnStarting(() => { if (!context.Response.Headers.ContainsKey("X-RateLimit-Limit")) { var isWrite = HttpMethods.IsPost(context.Request.Method); context.Response.Headers["X-RateLimit-Limit"] = isWrite ? "30" : "60"; } return Task.CompletedTask; }); await next(); }); ``` ## References - ASP.NET Core Middleware: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/ - HttpResponse.OnStarting: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.onstarting
View on GitHub