Skip to content

[Validation] HTTP HEAD requests to Razor component endpoints #68515

Description

@oroztocil

Scenario contact: @javiercn

Scenario

Uptime monitors, load balancer probes, link checkers and caches ask a page for its headers with an HTTP HEAD request. In .NET 10 Razor component endpoints advertised only GET and POST, so every one of those requests came back 405 Method Not Allowed, which showed up as false alerts and broken-link reports. .NET 11 adds HEAD to the endpoint's method metadata. A HEAD request now runs the page exactly as GET does and the server drops the body.

Minimum build

.NET 11 Preview 7 or later.

Configurations to cover

  • Blazor Web App
    • Static SSR
    • Interactive Server
    • Interactive WebAssembly
    • Interactive Auto
  • Standalone WebAssembly
  • Hybrid (MAUI)

The response to a HEAD request is produced entirely on the server, so the two configurations above are enough. Interactive WebAssembly and Auto reach the same server-side path as Interactive Server.

Also exercise

  • Published output
  • An existing .NET 10 app upgraded to .NET 11
  • Trimming or ahead-of-time compilation
  • More than one server instance, or a proxy in front
  • Hot Reload
  • An IDE as well as the command line
  • Container

Setup

You need curl, or an HTTP client that can send HEAD and show you raw response headers. Browsers cannot send HEAD from the address bar, so this scenario is driven from the command line.

What to build

One Blazor Web App with these routes. Keep them trivial; the point is the response, not the page.

Route What it is
/plain A page with a heading and nothing else
/item/42 A page taking a route parameter, rendering the value
/slow A page using @attribute [StreamRendering] that awaits 2 seconds before showing data
/secure A page with @attribute [Authorize]
/teapot A page that sets HttpContext.Response.StatusCode = 418 and adds a header X-Test: hello
/missing-route Nothing. No page declares this route
/logo.png Any static file, as a control that never went through the component endpoint

Add a counter that every page increments when it renders, so you can tell whether a request executed the page or merely returned headers:

// Program.cs
builder.Services.AddSingleton<HitCounter>();
...
app.MapGet("/hits", (HitCounter c) => c.Count);

public class HitCounter
{
    private int _count;
    public int Count => _count;
    public void Increment() => Interlocked.Increment(ref _count);
}

Inject it into each page and call Counter.Increment() from OnInitialized.

Things to try

Send each route both ways and compare. curl -I sends HEAD; curl -s -o /dev/null -D - sends GET and prints only the headers, so the two outputs are directly comparable:

curl -I http://localhost:5000/plain                     # HEAD
curl -s -o /dev/null -D - http://localhost:5000/plain   # GET, headers only

Use curl -I rather than curl -X HEAD. The latter sends the right method but then waits for a body that never arrives, so it hangs until it times out.

Work through the table above with both commands, then:

  • Read /hits before and after a HEAD request to /plain, to see whether the page ran.
  • Time the HEAD request to /slow with curl -o /dev/null -w '%{time_total}\n' -I http://localhost:5000/slow and compare it with the same GET.
  • Repeat the /plain and /secure requests against the app running on .NET 10, where both return 405.
  • With the app running under Hot Reload, add a new @page route and change an existing one, then send HEAD to both without restarting.

Expected behavior

A HEAD request is answered exactly as the matching GET would be, minus the response body.

Must hold

  • HEAD /plain returns 200, Content-Type: text/html; charset=utf-8, and an empty body.
  • For every route in the table, the status code returned by HEAD matches the status code returned by GET: 200 for /plain, /item/42 and /slow, 418 for /teapot, and whatever /secure returns when signed out, which is normally a 302 redirect to the login page.
  • HEAD /teapot carries the X-Test: hello header, so a page's own headers survive.
  • HEAD /missing-route, which matches no page, returns 404 rather than 405.
  • /hits increases by one after a HEAD request, confirming the page executed rather than being short-circuited.
  • HEAD /slow takes about as long as GET /slow, since streaming rendering still runs.
  • No response to a HEAD request carries a body, and no HEAD request produces a 405.
  • Nothing appears in the server log for a HEAD request that does not also appear for the matching GET.

Expected differences between configurations

  • None between Static SSR and Interactive Server. If a status code or header differs between them for the same route, that is a finding.
  • On .NET 10 every one of these returns 405. That is the behavior being fixed, not a failure.

Evidence to capture

For each route, the full HEAD and GET header output pasted as text, so the two can be compared line by line. Include the /hits values read before and after.

Documentation to use

What to report

Report results using the format described in the validation testing manual. Include link to a repository with the test app.

Metadata

Metadata

Assignees

Labels

ValidationThis issue is used to track validation effortsarea-blazorIncludes: Blazor, Razor Componentsvalidation-scenarioThis issue describes a validation testing scenario

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions