ASP.NET Core 11 Breaking Changes: Kestrel, OTel, Blazor

Last updated: November 11, 2026 · Tested with ASP.NET Core 11.0.0 (SDK 11.0.100) and ASP.NET Core 10.0.12 (SDK 10.0.401), ASP.NET Core 10.0.5 runtime for the servicing checks, Microsoft.AspNetCore.Identity.EntityFrameworkCore and QuickGrid 10.0.12 / 11.0.0, Windows 11

Short answer: the ASP.NET Core 11 breaking changes that change real requests are these. Over HTTP/2, Kestrel now drops the whole connection with GOAWAY PROTOCOL_ERROR when a request carries Proxy-Connection, Keep-Alive, Transfer-Encoding or Upgrade. The hosting Activity gets OpenTelemetry tags and a new display name. Response compression adds Vary: Accept-Encoding to uncompressed text. A Blazor SSR app without UseAntiforgery() stops failing with 500 and accepts form posts with no token. QuickGrid renders links. Removed obsolete APIs fail the build.

Two items on Microsoft’s list didn’t change anything between 10.0.12 and 11 in my runs, because the fixes were also shipped to .NET 10. Details below.

This is part 2. The .NET 11 upgrade post covers the ConcurrencyLimiter and MVC compatibility removals, Microsoft.OpenApi 3.x and the SqlClient Entra ID package. The OpenAPI 3.2 post covers the OpenAPI document changes, including all [ProducesResponseType] entries per status code and the server URL without a trailing slash. I don’t repeat them here.

Symptom on ASP.NET Core 11CauseFix
HTTP/2 client gets GOAWAY PROTOCOL_ERROR, all streams on the connection failRequest has Proxy-Connection, Keep-Alive, Transfer-Encoding or UpgradeStrip connection-specific headers in the proxy or client
Server spans renamed to GET /orders/{id:int}, extra tags, 5xx spans marked ErrorOpenTelemetry tags are on by defaultUpdate dashboards and enrichers, or set SuppressActivityOpenTelemetryData to true
http.route for MVC is Store/Checkout/{id?} in spans and metricsConventional route values are filled inCheck metric cardinality; this one can’t be switched off
Uncompressed responses now carry Vary: Accept-EncodingCompression middleware adds it for every compressible typeMake sure the CDN normalizes Accept-Encoding
Blazor SSR form works without UseAntiforgery(); no token in the formThe endpoint no longer validates tokens itselfCall app.UseAntiforgery() if you want token checks
UI tests can’t find QuickGrid button elementsSort and paging controls are <a href> linksUpdate selectors, or set the QuickGrid AppContext switch to false
CS1729, CS1061, CS1503, CS7036 on old MVC and Blazor APIsObsolete members removedUse the replacement overloads
MissingMethodException from a library built for .NET 10Same removal, binary sideRebuild the library against .NET 11

How I Tested It

One small project per change, each built twice from the same source. A property in Directory.Build.props picks the target and the package version. net10.0 builds with the system SDK 10.0.401, and net11.0 builds with the .NET 11 SDK 11.0.100:

<AspVersion Condition="'$(TF)' == 'net10.0'">10.0.12</AspVersion>
<AspVersion Condition="'$(TF)' == 'net11.0'">11.0.0</AspVersion>

Every server test starts real Kestrel on localhost and sends requests over TCP from the same process. For the HTTP/2 tests I wrote the frames by hand, so I control exactly which headers go on the wire. Two of the changes were also fixed in .NET 10 patches. For those I ran the .NET 10 build a second time with its runtimeconfig pinned to the 10.0.5 runtime, which was also installed. All output below is copied from those runs.

1. Kestrel Rejects Connection-Specific Headers Over HTTP/2

HTTP/2 doesn’t use Connection-style headers. RFC 9113 says a request that contains them is malformed. Kestrel 10 only enforced that for Connection and for TE with a value other than trailers. Kestrel 11 enforces the full list.

The test opens an h2c connection (HTTP/2 without TLS, prior knowledge) to an endpoint set to HttpProtocols.Http2. It sends one GET / with one extra header, written as a plain HPACK literal:

void Literal(string n, string v) // literal header field without indexing, new name, no Huffman
{
    hpack.WriteByte(0); hpack.WriteByte((byte)n.Length); hpack.Write(Encoding.ASCII.GetBytes(n));
    hpack.WriteByte((byte)v.Length); hpack.Write(Encoding.ASCII.GetBytes(v));
}
Literal(":method", "GET"); Literal(":scheme", "http"); Literal(":path", "/"); Literal(":authority", "localhost:5102");
if (extraName != "") Literal(extraName, extraValue);

Here is what came back:

.NET 10: (no extra header)                -> HEADERS :status 200
.NET 10: te: trailers                     -> HEADERS :status 200
.NET 10: connection: keep-alive           -> GOAWAY PROTOCOL_ERROR
.NET 10: te: gzip                         -> GOAWAY PROTOCOL_ERROR
.NET 10: proxy-connection: keep-alive     -> HEADERS :status 200
.NET 10: keep-alive: timeout=5            -> HEADERS :status 200
.NET 10: transfer-encoding: chunked       -> HEADERS :status 200
.NET 10: upgrade: websocket               -> HEADERS :status 200

.NET 11: (no extra header)                -> HEADERS :status 200
.NET 11: te: trailers                     -> HEADERS :status 200
.NET 11: connection: keep-alive           -> GOAWAY PROTOCOL_ERROR
.NET 11: te: gzip                         -> GOAWAY PROTOCOL_ERROR
.NET 11: proxy-connection: keep-alive     -> GOAWAY PROTOCOL_ERROR
.NET 11: keep-alive: timeout=5            -> GOAWAY PROTOCOL_ERROR
.NET 11: transfer-encoding: chunked       -> GOAWAY PROTOCOL_ERROR
.NET 11: upgrade: websocket               -> GOAWAY PROTOCOL_ERROR

I expected a stream reset, because the Learn page says Kestrel “resets the stream”. That was wrong. In my runs Kestrel sent GOAWAY, which closes the whole connection. Every other request multiplexed on that connection fails too. There is no HTTP status code and no log line at the default level. The reason only shows up at Debug:

log Debug Http2ConnectionError: Connection id "0HNPQ2K7NCJFS": HTTP/2 connection error. | Http2ConnectionErrorException: HTTP/2 connection error (PROTOCOL_ERROR): Request headers contain connection-specific header field.

Microsoft says compliant HTTP/2 clients don’t send these headers. The likely cause is a gateway or a custom proxy that copies HTTP/1.1 request headers into an HTTP/2 request. If clients report random connection drops after the upgrade, set Microsoft.AspNetCore.Server.Kestrel to Debug and look for Http2ConnectionError.

2. Content-Length Plus Transfer-Encoding Closes the Connection

The second half of the Kestrel change covers HTTP/1.1. A request with both framing headers is still processed, with Content-Length ignored, but the server then closes the connection. I sent this, then a second request on the same socket:

var request = "POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";

I expected this to be a 10-vs-11 difference. It isn’t. ASP.NET Core 10.0.12 and 11.0.0 behave the same. Both answer the first request and close the connection, so the second request on it fails:

HTTP/1.1 200 OK
Connection: close
body 'hello', Content-Length header present: False
response 2: IOException: Unable to read data from the transport connection: An established connection was aborted by the software in your host machine..

The .NET 10 build pinned to the 10.0.5 runtime kept the connection open and answered the second request:

runtime 10.0.5, ASP.NET Core 10.0.5
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
response 2:
ok HTTP/1.1

The Learn page has a note saying the fix also ships in servicing updates for earlier versions. So whether you see this depends on your .NET 10 patch, not on the upgrade. A client that sends both headers is broken anyway. Send only Transfer-Encoding: chunked.

3. Hosting Activity Carries OpenTelemetry Tags

The Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData switch now defaults to false. An ActivityListener on the Microsoft.AspNetCore source prints what changes, with no collector involved. Here is GET /orders/42:

.NET 10: Activity Microsoft.AspNetCore.Hosting.HttpRequestIn, DisplayName 'Microsoft.AspNetCore.Hosting.HttpRequestIn', Status Unset, StatusDescription ''
.NET 11: Activity Microsoft.AspNetCore.Hosting.HttpRequestIn, DisplayName 'GET /orders/{id:int}', Status Unset, StatusDescription ''
client.address = ::1
http.request.method = GET
http.response.status_code = 200
http.route = /orders/{id:int}
network.peer.address = ::1
network.protocol.version = 1.1
server.address = localhost
server.port = 5201
url.path = /orders/42
url.scheme = http
user_agent.original = probe/1.0

.NET 10 had no tags at all. The tags are documented. The DisplayName change isn’t spelled out, and it’s the one that breaks things. Exporters use it as the span name, so a filter or alert on Microsoft.AspNetCore.Hosting.HttpRequestIn stops matching. The tags also include client.address, url.path and user_agent.original. Check whether those may go to your tracing backend.

The MVC route changes in the span and in the http.server.request.duration metric:

.NET 10: metric http.server.request.duration: http.request.method=GET, http.response.status_code=200, http.route={controller=Home}/{action=Index}/{id?}, network.protocol.version=1.1, url.scheme=http
.NET 11: metric http.server.request.duration: http.request.method=GET, http.response.status_code=200, http.route=Store/Checkout/{id?}, network.protocol.version=1.1, url.scheme=http

One time series per controller action instead of one per route template. A 503 returned with Results.StatusCode(503) now marks the span as an error and adds error.type:

.NET 10: metric http.server.request.duration: http.request.method=GET, http.response.status_code=503, http.route=/unavailable, network.protocol.version=1.1, url.scheme=http
.NET 11: metric http.server.request.duration: error.type=503, http.request.method=GET, http.response.status_code=503, http.route=/unavailable, network.protocol.version=1.1, url.scheme=http
.NET 11: Activity Microsoft.AspNetCore.Hosting.HttpRequestIn, DisplayName 'GET /unavailable', Status Error, StatusDescription ''

Another wrong assumption: I expected .NET 10 to put the exception message in StatusDescription, because the Learn page says 11 stopped doing that. My endpoint threw InvalidOperationException("Database password is hunter2"). On 10.0.12 the span had Status Unset and an empty description, with the switch at either value. On 11 it’s Status Error, still with no description. My listener saw no exception event on either version.

Setting the switch back to true on 11 removes the tags and restores the old display name. It doesn’t restore the old route or the error.type=503 dimension:

SuppressActivityOpenTelemetryData set to true
metric http.server.request.duration: http.request.method=GET, http.response.status_code=200, http.route=Store/Checkout/{id?}, network.protocol.version=1.1, url.scheme=http
Activity Microsoft.AspNetCore.Hosting.HttpRequestIn, DisplayName 'Microsoft.AspNetCore.Hosting.HttpRequestIn', Status Unset, StatusDescription ''

If you use OpenTelemetry.Instrumentation.AspNetCore, update it before you upgrade. Microsoft says the latest version detects the built-in tags and doesn’t add them twice. I didn’t test that package.

4. Response Compression Adds Vary to Uncompressed Responses

Default AddResponseCompression(), one text endpoint, one PNG, and one text endpoint that sets Vary: Origin itself. Each was requested with and without Accept-Encoding: gzip:

.NET 10: /text      Accept-Encoding: gzip   -> 200   116 bytes, Content-Encoding: gzip   Vary: Accept-Encoding
.NET 10: /text      Accept-Encoding: (none) -> 200  5600 bytes, Content-Encoding: (none) Vary: (none)
.NET 10: /logo.png  Accept-Encoding: gzip   -> 200  5000 bytes, Content-Encoding: (none) Vary: (none)
.NET 10: /cors-text Accept-Encoding: (none) -> 200  5600 bytes, Content-Encoding: (none) Vary: Origin

.NET 11: /text      Accept-Encoding: gzip   -> 200   116 bytes, Content-Encoding: gzip   Vary: Accept-Encoding
.NET 11: /text      Accept-Encoding: (none) -> 200  5600 bytes, Content-Encoding: (none) Vary: Accept-Encoding
.NET 11: /logo.png  Accept-Encoding: gzip   -> 200  5000 bytes, Content-Encoding: (none) Vary: (none)
.NET 11: /cors-text Accept-Encoding: (none) -> 200  5600 bytes, Content-Encoding: (none) Vary: Origin, Accept-Encoding

The .NET 10 behaviour was a real bug. A shared cache that stored the uncompressed /text response, which had no Vary, could serve it to every client, and a cache that stored the gzip version could send gzip to a client that didn’t ask for it. On 11 both variants say Vary: Accept-Encoding, so the cache keys on the header.

The cost is more variants. Clients send gzip, deflate, br, gzip, br, zstd and other mixes. A cache that keys on the raw header stores one copy per string. Most CDNs normalize Accept-Encoding before they look it up, but check your own reverse proxy. Assertions on the exact Vary value also break, as the Origin, Accept-Encoding line shows.

The Learn page says the header goes on “every response that passes through it”. The PNG says otherwise. image/png isn’t in the default MIME types, and on 11 it still has no Vary. In my runs the header was added only for content types the middleware would compress.

5. Blazor SSR Without UseAntiforgery Accepts Tokenless Posts

A static SSR page with an EditForm, AddRazorComponents() and MapRazorComponents<App>(), with and without app.UseAntiforgery(). I expected the “before” on the Learn page: .NET 10 validating tokens in the endpoint even without the middleware. That isn’t what 10.0.12 does. Without the middleware every request fails, including the GET:

.NET 10: GET /contact: 500, token field rendered: False, antiforgery cookie: False

The log has the familiar InvalidOperationException: the endpoint “contains anti-forgery metadata, but a middleware was not found”. The same app on 11 runs, renders no token, and accepts posts that don’t have one:

.NET 11: pipeline: no UseAntiforgery call
.NET 11: GET /contact: 200, token field rendered: False, antiforgery cookie: False
.NET 11: POST, no token, no browser headers: 200 OK, result 'saved Ann'
.NET 11: POST, no token, Sec-Fetch-Site: same-origin: 200 OK, result 'saved Ann'
.NET 11: POST, no token, Sec-Fetch-Site: cross-site: 400 Bad Request, result ''
.NET 11: POST, no token, Origin: https://evil.example: 400 Bad Request, result ''

Protection now comes from the new cross-origin CSRF middleware, which WebApplication.CreateBuilder adds for you. It looks at Sec-Fetch-Site and Origin, not at tokens. Its rejection is a Debug log line:

dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1] Cross-origin CSRF protection marked request POST /contact from origin 'https://evil.example' as invalid.

With app.UseAntiforgery() both versions behave the same: the token is rendered, every tokenless post gets 400, and the post with the token returns saved Ann. So this only matters for apps that never called UseAntiforgery(). On .NET 10 such an app was broken. On .NET 11 it works, and you may not notice that form posts have no token check. A request with no Origin and no Sec-Fetch-Site header, from a script for example, goes through. If you want token checks, add the call.

6. QuickGrid Sorting and Paging Render as Links

Same app, a QuickGrid with a sortable column and a Paginator, rendered with static SSR:

.NET 10: <button class="col-title" type="button" b-b7yucal7gj>
.NET 10: <button class="go-next" type="button" title="Go to next page" aria-label="Go to next page" b-3qssc0bm46>
.NET 11: <a class="col-title" href="http://localhost:5401/grid?sort=Name&amp;direction=asc" b-b7yucal7gj>
.NET 11: <a class="go-next" href="http://localhost:5401/grid?page=2" title="Go to next page" aria-label="Go to next page" aria-disabled="false" b-3qssc0bm46>

On 10 those buttons do nothing without an interactive render mode. On 11 the links work under static SSR:

.NET 10: GET /grid?page=2: first row 'Person 01'
.NET 11: GET /grid?page=2: first row 'Person 11'
.NET 11: GET /grid?sort=Name&direction=desc: first row 'Person 30'

Two things to check. Selectors such as nav button:disabled no longer match; disabled links use aria-disabled="true". And the grid now reads page, sort and direction from the query string, so a page that already uses ?page= for something else will clash. To get buttons back, set Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting to false.

7. Passkey Sign-In Checks: Already in .NET 10

The list says PasskeySignInAsync now runs PreSignInCheck, so an unconfirmed or locked-out user can’t sign in with a passkey. I can’t generate a real WebAuthn assertion without a browser, so I replaced IPasskeyHandler<IdentityUser> with a fake that accepts any assertion. Everything else is the real Identity code with EF Core on in-memory SQLite: options request, state cookie, PasskeySignInAsync, auth cookie. RequireConfirmedEmail was true. All three runtimes gave the same result:

.NET 10: == unconfirmed@example.com (EmailConfirmed=False, locked out=False), RequireConfirmedEmail=true
.NET 10: PasskeySignInAsync  -> NotAllowed, auth cookie issued: False
.NET 10: == locked@example.com (EmailConfirmed=True, locked out=True), RequireConfirmedEmail=true
.NET 10: PasskeySignInAsync  -> LockedOut, auth cookie issued: False
runtime 10.0.5, ASP.NET Core 10.0.5
.NET 11: PasskeySignInAsync  -> NotAllowed, auth cookie issued: False
.NET 11: PasskeySignInAsync  -> LockedOut, auth cookie issued: False

The fix (dotnet/aspnetcore #65024) was backported to release/10.0 as #65316 in February 2026. If your .NET 10 app is patched, this is not a new break. If passkey users suddenly got NotAllowed after a .NET 10 patch, this is why. Confirm their email rather than removing the check.

8. Removed Obsolete APIs

One file that uses the old members builds on 10 with CS0618 warnings. On 11 it fails:

Program.cs(20,11): error CS1729: 'ImageTagHelper' does not contain a constructor that takes 5 arguments
Program.cs(22,32): error CS1061: 'LegacyImageTagHelper' does not contain a definition for 'HostingEnvironment' and no accessible extension method 'HostingEnvironment' accepting a first argument of type 'LegacyImageTagHelper' could be found (are you missing a using directive or an assembly reference?)
Program.cs(37,58): error CS1503: Argument 1: cannot convert from 'System.Type' to 'System.Reflection.PropertyInfo'
Program.cs(40,21): error CS1061: 'EditContext' does not contain a definition for 'AddDataAnnotationsValidation' and no accessible extension method 'AddDataAnnotationsValidation' accepting a first argument of type 'EditContext' could be found (are you missing a using directive or an assembly reference?)
Program.cs(41,21): error CS7036: There is no argument given that corresponds to the required parameter 'serviceProvider' of 'EditContextDataAnnotationsExtensions.EnableDataAnnotationsValidation(EditContext, IServiceProvider)'
Program.cs(44,16): error CS1061: 'Router' does not contain a definition for 'PreferExactMatches' and no accessible extension method 'PreferExactMatches' accepting a first argument of type 'Router' could be found (are you missing a using directive or an assembly reference?)

The CS1503 is the ModelMetadataIdentity.ForProperty(Type, string, Type) overload. The compiler picks the remaining PropertyInfo overload, so the message doesn’t mention the removal. A library compiled for .NET 10 and loaded into an 11 app fails only when the method runs:

MissingMethodException: Method not found: 'Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity.ForProperty(System.Type, System.String, System.Type)'.

Custom model metadata providers and tag helper libraries are the usual places for these. Rebuild them against .NET 11 instead of trusting roll-forward.

What I Didn’t Test

  • Blazor custom event registration throwing on a name collision, and enhanced navigation preloading: both run in the browser’s JavaScript, and I tested without a browser.
  • WebAssemblyHostBuilder loading environment variables: needs the WebAssembly runtime.
  • HttpSys TLS channel binding: needs HTTP.sys with a TLS certificate binding, which needs admin rights.
  • The WebAssembly authentication removals (SignOutSessionStateManager and others), which live in a package I didn’t reference.

Checklist

  • Find gateways or proxies that forward HTTP/1.1 headers into HTTP/2 requests to Kestrel.
  • Log Microsoft.AspNetCore.Server.Kestrel at Debug if clients see connection drops.
  • Fix any client that sends both Content-Length and Transfer-Encoding. Current .NET 10 patches close the connection too.
  • Update span-name filters for the new DisplayName, and check that client.address and user_agent.original are allowed in traces.
  • Check metric cardinality for http.route and error.type.
  • Update OpenTelemetry.Instrumentation.AspNetCore.
  • Confirm your CDN or proxy normalizes Accept-Encoding, and update tests that assert on Vary.
  • Call app.UseAntiforgery() in Blazor SSR apps that rely on tokens.
  • Update QuickGrid CSS and UI tests, and watch for page/sort query-string clashes.
  • Rebuild libraries that use ImageTagHelper, ModelMetadataIdentity.ForProperty or old Blazor form APIs.

Related: upgrading to .NET 11: 9 breaking changes · ASP.NET Core 11 and OpenAPI 3.2 · Microsoft’s ASP.NET Core 11 breaking changes list · Kestrel protocol compliance on Learn · all ASP.NET Core tutorials

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *