Skip to content

Accept IPv6 literal hosts in the managed HttpListener - #135937

Open
pcshrosbree wants to merge 1 commit into
dotnet:mainfrom
pcshrosbree:fix/httplistener-ipv6-literal
Open

pcshrosbree wants to merge 1 commit into
dotnet:mainfrom
pcshrosbree:fix/httplistener-ipv6-literal

Conversation

@pcshrosbree

@pcshrosbree pcshrosbree commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #34399

On Linux and macOS, HttpListener rejects a prefix with an IPv6 literal such as http://[::1]:8080/: Start throws HttpListenerException (400): Invalid port in prefix. On Windows, http.sys accepts it.

What was wrong

The managed implementation finds the port of a host by looking for the first :. In an IPv6 literal that colon is inside the brackets. This affected both prefix registration and request parsing:

  • The prefix. HttpEndPointManager.AddPrefixInternal and ListenerPrefix.Parse read :1]:8080 as the port.
  • The request. HttpListenerRequest.FinishInitialization also splits the host at the first :. A request with Host: [::1]:8080, which is what a client sends to an [::1] prefix, therefore gets 400 Bad Request before it reaches the application. The same problem affects absolute-form requests for [::1] and HTTP/1.0 requests without a Host header that arrive over IPv6.

What changed

  • For a host that starts with [, the new helper ListenerPrefix.IndexOfPortSeparator finds the port separator at the first : after the closing ]. All three methods above use it. Hosts that don't start with [ are split as before, and the port checks are unchanged.
  • ListenerPrefix normalizes IPv6 literals to match Uri.Host, so a prefix containing [0:0:0:0:0:0:0:1] matches a request for [::1].
  • Literals with a scope ID, such as [fe80::1%25eth0], are still rejected. Uri.Host drops the scope ID while binding needs it, so supporting them needs a decision of its own.
  • Binding didn't need a change: Dns.GetHostAddresses returns the address of an IP literal without a DNS lookup, so the listener binds to ::1.
  • The other point in the issue, that localhost listens on both address families on Windows but not on Linux, is a separate change and isn't in this PR.

A malformed bracketed host such as [::1]x:5 can now be reported as an invalid host instead of an invalid port; both errors return 400. The [::] literal now fails with error 50, like 0.0.0.0, instead of an invalid-port error.

On Windows the library uses http.sys and doesn't compile the three source files I changed.

Tests

  • HttpListenerIPv6Tests checks that both [::1] and [0:0:0:0:0:0:0:1] prefixes serve requests over IPv6 loopback with the expected Request.Url, UserHostName and LocalEndPoint. It also checks Request.Url for origin-form, absolute-form and HTTP/1.0 requests, and that a + prefix accepts a request with Host: [::1]:port.
  • [::1] added to Hosts_TestData, so the existing prefix registration tests run with it.
  • Prefixes with invalid ports, an unclosed [, or a scoped literal are still rejected.
  • Tests that listen on an [::1] prefix run only where ::1 can be bound. With http.sys, they also require elevation, because registering an IP literal prefix may require it. The wildcard test uses the existing check for wildcard prefixes.

I ran the whole System.Net.HttpListener.Tests project (Release) on main, on main with the new tests, and with this change:

main main + new tests this PR
linux-x64 0 failed of 673 12 failed of 699 0 failed of 699
osx-arm64 0 failed of 669 12 failed of 695 0 failed of 695
windows-x64 1 failed of 708 1 failed of 734 1 failed of 734

In the main + new tests runs, the 12 failures are from the new IPv6 tests; the new rejected-prefix tests pass. The Windows runs were elevated. The Windows failure is ExtendedProtectionSelectorDelegate_IncreasesPolicyBetweenNtlmLegs_AuthenticationFails, which fails on that machine on main as well; all the new tests pass there. I also undid each part of the fix on its own and checked that at least one of the new tests fails each time.

Note

This PR was prepared with AI assistance (Anthropic Claude, checked by OpenAI Codex) at my request. I reviewed it before opening it.

On Linux and macOS, adding a prefix such as http://[::1]:8080/ makes
Start throw HttpListenerException "Invalid port in prefix", while
http.sys on Windows accepts it. The managed implementation finds the
port by searching for the first ':' after the scheme, and for an IPv6
literal, that is a colon inside the brackets. The same search also
splits IPv6 request hosts at a colon inside the brackets, so a request
with "Host: [::1]:8080" gets 400 Bad Request before it reaches the
application. The same split also breaks absolute-form requests for
[::1] and HTTP/1.0 requests without a Host header that arrive over IPv6.

For a host that starts with '[', find the port separator by looking for
the first ':' after the closing ']'. Apply this rule in
HttpEndPointManager.AddPrefixInternal, ListenerPrefix.Parse and
HttpListenerRequest.FinishInitialization. Hosts that do not start with
'[' are split as before, and the port checks are unchanged. A malformed
bracketed host such as [::1]x can now be reported as an invalid host
rather than an invalid port; both are 400.

ListenerPrefix now normalizes IPv6 literals to match Uri.Host, so a
prefix containing [0:0:0:0:0:0:0:1] matches a request for [::1].

Scope IDs remain unsupported and now report an invalid-host error.
The [::] literal is now rejected with error 50, like 0.0.0.0, instead
of 400.

Binding needed no change: Dns.GetHostAddresses returns a literal such as
"[::1]" without a lookup, so the listener binds to ::1.

This change does not make localhost listen on both address families
on Linux, as it does on Windows.

Add tests for IPv6 loopback prefixes and requests, and for prefixes
that must still be rejected: bad ports, an unclosed '[' and scoped
literals. The full System.Net.HttpListener.Tests project passes in
Release on linux-x64 and osx-arm64; on windows-x64 the new tests pass,
and the one failing test fails on main too.

Fixes dotnet#34399
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Oct 16, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@MihaZupan

Copy link
Copy Markdown
Member

I'm inclined to say won't-fix here. This never worked so it's not a regression, and a clear better alternative exists - Kestrel.
IMO it's not worth the extra maintenance burden / risk of more bugs being introduced into this type.

See also
https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-10.0#remarks

We recommend that you don't use the HttpListener class for new development

@pcshrosbree

pcshrosbree commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

TL;DR: I opened this PR because #34399 had been open for six years and a commenter had pointed to its cause in February 2025, not because my own work needs the fix. Your points stand: it isn't a regression, and Kestrel is the recommended alternative. Would you prefer me to close this PR? If so, closing #34399 as not planned would tell the next person who finds it where things stand.

Thanks for looking at it. Some background on why I opened it.

This fix isn't something my own work needs. I hit the bug while writing docs for the OpenTelemetry Prometheus exporter that is built on HttpListener, and found #34399. It was reported in April 2020, labeled a bug and moved to Future the next month, and has had no assignee since. In February 2025 a commenter pointed to the cause: AddPrefixInternal takes the first : as the start of the port. With the cause known, and an issue that had stayed open for six years, a fix seemed worth offering.

Your points stand: it never worked on Linux or macOS, so it isn't a regression, and Kestrel is the recommended alternative. Would you prefer me to close this PR? If so, closing #34399 as not planned would tell the next person who finds it where things stand.

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-System.Net.Http community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IPv6 System.Net.HttpListenerException (400): Invalid port in prefix.

2 participants