Skip to content

HttpContext & Features

Key Points

  • HttpContext is the per-request facade carrying everything: Request, Response, User, Items, RequestServices, RequestAborted, Connection, Features.
  • IHttpContextAccessor lets non-controller code access the current HttpContext. Use sparingly — request-scoped DI is usually cleaner.
  • Features is an extensibility collection where servers/middleware publish capabilities (e.g., IHttpRequestFeature, IHttpResponseStartFeature).
  • Don't capture HttpContext across awaits if you don't need to — the request may end before your continuation runs.
  • Items dictionary 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 HttpContext in 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 HttpContext past 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 HttpContext directly.

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 HttpContext in controllers/middleware directly.
  • Use IHttpContextAccessor sparingly — favor scoped services.
  • Always honor RequestAborted in long actions.
  • Avoid capturing HttpContext across 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 HttpContext across await in fire-and-forget — disposed.
  • ⚠️ Reading body without EnableBuffering — model binder gets empty.
  • ⚠️ Trusting RemoteIpAddress behind proxy — wrong without UseForwardedHeaders.
  • ⚠️ Cookies without HttpOnlyJS-readable.
  • ⚠️ Cookies without SameSiteCSRF risk.
  • ⚠️ IHttpContextAccessor in tight loops — AsyncLocal cost.

Further reading