HttpContext & Features
Key Points
HttpContextis the per-request facade carrying everything:Request,Response,User,Items,RequestServices,RequestAborted,Connection,Features.IHttpContextAccessorlets non-controller code access the currentHttpContext. Use sparingly — request-scoped DI is usually cleaner.Featuresis an extensibility collection where servers/middleware publish capabilities (e.g.,IHttpRequestFeature,IHttpResponseStartFeature).- Don't capture
HttpContextacross awaits if you don't need to — the request may end before your continuation runs. Itemsdictionary is per-request scratch space; useful for middleware → controller hand-off.
Concepts (deep dive)
Anatomy
public class C : ControllerBase
{
public IActionResult Foo()
{
var req = HttpContext.Request; // method, path, headers, body
var res = HttpContext.Response; // status, headers, body
var user = HttpContext.User; // ClaimsPrincipal
var items = HttpContext.Items; // request-scoped scratch
var sp = HttpContext.RequestServices; // request scope's IServiceProvider
var token = HttpContext.RequestAborted; // CancellationToken (client disconnect)
var conn = HttpContext.Connection; // remote IP, client cert
var features = HttpContext.Features; // server features
return Ok();
}
}
IHttpContextAccessor
public class TenantResolver(IHttpContextAccessor http)
{
public string? CurrentTenant
=> http.HttpContext?.User.FindFirst("tenant")?.Value;
}
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<TenantResolver>();
IHttpContextAccessor uses AsyncLocal<HttpContext> — flows across awaits. Useful in singletons that need to read per-request state.
⚠️ Caveats: AsyncLocal flow is real but easy to leak. Don't store
HttpContextin fields of singletons; capture only what you need (e.g., a string).
Items dictionary
// Middleware sets:
app.Use(async (ctx, next) =>
{
ctx.Items["RequestId"] = Guid.NewGuid().ToString();
await next(ctx);
});
// Action reads:
var id = HttpContext.Items["RequestId"] as string;
For typed access, use a static helper or extension method to avoid string typos.
RequestAborted
[HttpGet]
public async Task<IActionResult> Slow(CancellationToken ct) // bound from RequestAborted
{
await ExpensiveAsync(ct); // honor the cancel
return Ok();
}
When the client disconnects (closed connection, navigation away), RequestAborted cancels. Honor it in long operations to free resources.
Connection info
var ip = HttpContext.Connection.RemoteIpAddress; // beware of proxies
var port = HttpContext.Connection.RemotePort;
var clientCert = HttpContext.Connection.ClientCertificate; // mutual TLS
For real client IP behind a proxy, use UseForwardedHeaders middleware first. See Hosting & Kestrel.
Features collection
var lifetime = HttpContext.Features.Get<IHttpRequestLifetimeFeature>();
var trailers = HttpContext.Features.Get<IHttpResponseTrailersFeature>();
var resetFeature = HttpContext.Features.Get<IHttpResetFeature>(); // HTTP/2 RST_STREAM
The features are how Kestrel and middleware expose protocol-specific capabilities. Most apps don't touch them; libraries (signalr, gRPC) do.
Capturing across await — the trap
[HttpGet]
public async Task<IActionResult> Bad()
{
_ = Task.Run(async () =>
{
await Task.Delay(60_000);
var path = HttpContext.Request.Path; // ❌ HttpContext disposed by now
});
return Ok();
}
HttpContext is disposed when the request ends. Background work that captures it sees disposed state. Capture what you need into local variables before the boundary.
User and claims
var sub = HttpContext.User.FindFirst("sub")?.Value;
var isAdmin = HttpContext.User.IsInRole("admin");
var hasScope = HttpContext.User.HasClaim("scope", "orders.read");
See Security for auth deep dive.
Setting cookies
HttpContext.Response.Cookies.Append("theme", "dark", new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(30)
});
Always HttpOnly = true for any cookie containing identity/state. Secure = true in production. SameSite defends against CSRF.
Reading the body twice
HttpContext.Request.EnableBuffering();
using var reader = new StreamReader(HttpContext.Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
HttpContext.Request.Body.Position = 0; // rewind for next reader
By default the body is a forward-only stream. EnableBuffering() switches to a buffered/file-backed stream so middleware can read multiple times. Has memory cost — only enable when needed.
Code: correct vs wrong
❌ Wrong: capturing HttpContext for fire-and-forget
[HttpPost]
public IActionResult Process()
{
_ = Task.Run(async () => await ProcessAsync(HttpContext)); // ❌ disposed
return Accepted();
}
✅ Correct: capture what you need
[HttpPost]
public IActionResult Process(HttpContext ctx)
{
var userId = ctx.User.FindFirst("sub")?.Value!;
var ct = ctx.RequestAborted;
_ = Task.Run(async () => await ProcessAsync(userId, ct));
return Accepted();
}
(Better: enqueue to a Channel or external queue; don't fire-and-forget on the threadpool.)
❌ Wrong: ignoring RequestAborted
[HttpGet]
public async Task<IActionResult> Big()
{
var data = await LargeQueryAsync(); // ❌ no cancel; client gone, server still computes
return Ok(data);
}
✅ Correct
[HttpGet]
public async Task<IActionResult> Big(CancellationToken ct)
{
var data = await LargeQueryAsync(ct);
return Ok(data);
}
❌ Wrong: reading body without EnableBuffering
app.Use(async (ctx, next) =>
{
var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync(); // consumes stream
await next(ctx); // model binder gets empty body
});
✅ Correct
app.Use(async (ctx, next) =>
{
ctx.Request.EnableBuffering();
using var reader = new StreamReader(ctx.Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
ctx.Request.Body.Position = 0;
await next(ctx);
});
Design patterns for this topic
Pattern 1 — "Capture, don't reference"
- Intent: never hold
HttpContextpast the request.
Pattern 2 — "Items for middleware → controller hand-off"
- Intent: typed wrapper for cross-pipeline state.
Pattern 3 — "IHttpContextAccessor only when scoped DI doesn't fit"
- Intent: prefer scoped services that depend on
HttpContextdirectly.
Pattern 4 — "EnableBuffering only when reading body more than once"
- Intent: don't pay buffering cost for everything.
Pros & cons / trade-offs
| Approach | Pros | Cons |
|---|---|---|
HttpContext direct | Always available in actions/middleware | Disposed at request end |
IHttpContextAccessor | Access from anywhere | AsyncLocal cost; easy to misuse |
Items | Typed-free scratch | Stringly-typed without wrappers |
| Features | Protocol-level access | Most apps never need |
When to use / when to avoid
- Use
HttpContextin controllers/middleware directly. - Use
IHttpContextAccessorsparingly — favor scoped services. - Always honor
RequestAbortedin long actions. - Avoid capturing
HttpContextacross fire-and-forget Tasks. - Avoid reading body twice without
EnableBuffering.
Interview Q&A
Q1. What's HttpContext.RequestAborted? Cancellation token that fires when the client disconnects or the server is shutting down.
Q2. When is HttpContext disposed? At the end of the request. Captured references are stale after that.
Q3. What's IHttpContextAccessor? A service that exposes the current HttpContext via AsyncLocal<>. Used to access request state from non-controller code.
Q4. Why might Connection.RemoteIpAddress be wrong? Behind a reverse proxy, it's the proxy's IP. Use UseForwardedHeaders (with KnownProxies) to surface the real client.
Q5. What's the Features collection? Extensibility point where the server / middleware publish capabilities. Most apps don't touch it.
Q6. How do you read the body twice? HttpContext.Request.EnableBuffering() switches the stream to buffered/file-backed. Reset position to 0 between reads.
Q7. Why don't you store HttpContext in a singleton service field? The context is per-request and disposed at end. A singleton holding a reference would see stale/disposed state.
Q8. How do you set a cookie with secure defaults? Response.Cookies.Append(name, value, new CookieOptions { HttpOnly=true, Secure=true, SameSite=Strict, Expires=... }).
Q9. What's HttpContext.RequestServices? The DI container scoped to the current request. Equivalent to the scope's IServiceProvider.
Q10. How do you do "fire-and-forget" without leaking the request scope? Capture primitives (string IDs, etc.); enqueue to a Channel or external queue; let a BackgroundService process. Don't Task.Run capturing HttpContext.
Gotchas / common mistakes
- ⚠️ Capturing
HttpContextacrossawaitin fire-and-forget — disposed. - ⚠️ Reading body without
EnableBuffering— model binder gets empty. - ⚠️ Trusting
RemoteIpAddressbehind proxy — wrong withoutUseForwardedHeaders. - ⚠️ Cookies without
HttpOnly— JS-readable. - ⚠️ Cookies without
SameSite— CSRF risk. - ⚠️
IHttpContextAccessorin tight loops — AsyncLocal cost.