Skip to content

Avoid allocating HttpContext.Items in StatusCodePagesMiddleware when unused - #70179

Open
davidfowl wants to merge 1 commit into
mainfrom
perf/statuscodepages
Open

davidfowl wants to merge 1 commit into
mainfrom
perf/statuscodepages

Conversation

@davidfowl

@davidfowl davidfowl commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Avoid allocating HttpContext.Items in StatusCodePagesMiddleware when unused

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

StatusCodePagesMiddleware allocates HttpContext.Items on every request, even when nothing uses it.

Description

#62178 added context.Items[nameof(StatusCodePagesOptions)] = _options.PathFormat before next and Items.Remove(...) after it, so Blazor's EndpointHtmlRenderer can find the re-execute path (Items[...] as string).

With UseStatusCodePages() or UseStatusCodePagesWithRedirects(...), PathFormat is null. Writing that null still forces DefaultHttpContext to create its ItemsFeature, the ItemsDictionary and the inner Dictionary<object, object?> on every request, even when nothing else in the pipeline uses Items.

Reading Items[key] returns null for a missing key, the same as for a stored null. So when PathFormat is null and the request's Items has not been created yet (the context is exactly DefaultHttpContext and IItemsFeature is null), the write is skipped.

Note

One observable difference (raised in review): when PathFormat is null and Items is first materialized downstream, Items.ContainsKey(nameof(StatusCodePagesOptions)) / TryGetValue now return false instead of true with a null value. An OWIN env["StatusCodePagesOptions"] read would throw KeyNotFoundException instead of returning null. Items[key] reads, including Blazor's EndpointHtmlRenderer, are unaffected. This needs a maintainer decision on whether the internal key's presence is a contract.

Otherwise:

  • When PathFormat is set (re-execute), or Items already exists, the entry is written and removed exactly as before, with no extra feature lookups on the re-execute path.
  • After next, the entry is still removed if Items was created downstream, matching the old unconditional Remove.
  • Custom HttpContext implementations always take the original path.

Also adds a direct ProjectReference to Microsoft.AspNetCore.Http (for DefaultHttpContext). Diagnostics already depends on it through Routing, and sibling middlewares (HostFiltering, WebSockets, ResponseCaching, …) reference it the same way. No public API changes.

Benchmark

Local BenchmarkDotNet, 3 runs each, net12.0, 2 cores. Each op is new DefaultHttpContext() plus Invoke with a next that sets status 200 (ContextOnly ≈ 140 ns, 1024 B).

Case main Mean (3 runs) PR Mean (3 runs) Δ time main Alloc PR Alloc
Default_200 (UseStatusCodePages()) 334 / 327 / 327 ns (±6) 242 / 248 / 253 ns (±3–5) −25% 1312 B 1048 B (middleware 288 → 24 B)
Default_200_NextUsesItems 359 / 346 / 346 ns 359 / 343 / 346 ns — 1312 B 1312 B
ReExecute_200 (PathFormat set) 332 / 326 / 353 ns 341 / 340 / 353 ns noise (same code path) 1312 B 1312 B

Tests

  • dotnet test src/Middleware/Diagnostics/test/UnitTests: 100/100 pass.

  • dotnet test src/Middleware/Diagnostics/test/FunctionalTests: 14 passed, 1 skipped (same as main).

  • New StatusCodeMiddlewareTest cases:

    • next sees null with no re-execute.
    • next sees the path format with re-execute.
    • An entry that already exists is overwritten with null and removed.
    • An entry added by next is removed afterwards.
    • Items is not allocated when unused.

    The first four also pass on main and lock in the existing behavior.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

…unused

When there is no re-execute path format (UseStatusCodePages / WithRedirects), writing a null
entry to HttpContext.Items forced DefaultHttpContext to allocate its ItemsFeature and the
backing dictionary on every request. A null value reads the same as a missing entry, so skip
the write when Items has not been allocated yet; the entry is still removed after next.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 31, 2026 09:48
@github-actions github-actions Bot added the area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares label Oct 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

When PathFormat is null, the change can omit the StatusCodePagesOptions key, causing downstream OWIN indexer reads to throw instead of returning null.

1 open issue
What changed in this PR

Reduces per-request allocations in Diagnostics’ StatusCodePagesMiddleware by avoiding initialization of unused HttpContext.Items when no re-execution path is configured.

Changes:

  • Conditionally writes the path-format entry and removes it if items are created downstream.
  • Adds tests for path-format visibility, cleanup, and avoiding unused-items allocation.
  • Adds a direct reference to the HTTP implementation project for DefaultHttpContext.
File Description
src/​Middleware/​Diagnostics/​test/​UnitTests/​StatusCodeMiddlewareTest.cs Adds five tests covering item handling and allocation avoidance.
src/​Middleware/​Diagnostics/​src/​StatusCodePage/​StatusCodePagesMiddleware.cs Skips unnecessary item initialization and conditionally cleans up the entry.
src/​Middleware/​Diagnostics/​src/​Microsoft.AspNetCore.Diagnostics.csproj References Microsoft.AspNetCore.Http directly.

Review effort: Balanced

// Attach pathFormat to HttpContext.Items early in the pipeline. A null value reads the same as a missing entry,
// so when there is no pathFormat and nothing has used Items yet, skip the write to avoid allocating Items.
var pathFormat = _options.PathFormat;
var setPathFormat = pathFormat is not null || !IsItemsUnallocated(context);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, thanks. I confirmed it in code: with this change, when PathFormat is null and Items is first materialized downstream, Items.ContainsKey(nameof(StatusCodePagesOptions)) / TryGetValue return false instead of true with a null value. OwinEnvironment's indexer (src/Http/Owin/src/OwinEnvironment.cs, lines 123–127) falls through to _context.Items.TryGetValue and then throws KeyNotFoundException, so an OWIN read of that key would throw instead of returning null.

Plain Items[key] reads, including the only in-repo consumer (EndpointHtmlRenderer.EventDispatch.cs, Items[...] as string), are unaffected because ItemsDictionary returns null for missing keys. Still, the PR description's "everything else is unchanged" was inaccurate. I've updated the description to call out this difference explicitly.

I haven't found a way to keep the key present when Items is materialized later without allocating Items up front (DefaultHttpContext exposes no hook for that), which is exactly the allocation this PR removes. I'm leaving this thread open so a maintainer can decide whether presence of this internal, null-valued key during the downstream pipeline needs to be preserved. If it does, this PR can't be made behavior-identical and shouldn't go in.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-middleware Includes: URL rewrite, redirect, response cache/compression, session, and other general middlewares

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants