Skip to content

[wasi] Enable SocketsHttpHandler and WebSockets - #136234

Draft
lewing wants to merge 13 commits into
dotnet:mainfrom
lewing:lewing-wasi-sockets-http-handler
Draft

lewing wants to merge 13 commits into
dotnet:mainfrom
lewing:lewing-wasi-sockets-http-handler

Conversation

@lewing

@lewing lewing commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Enables SocketsHttpHandler, System.Net.WebSockets and ClientWebSocket on WASI over wasi:sockets. WASI has no System.Net.Security, so only http:// and ws:// work. This lets WASI apps and tools (for example a dotnet watch agent) talk to a local server without wasi:http, which can't upgrade a connection to a WebSocket.

The main parts:

  1. Enable SocketsHttpHandler and WebSockets on WASI. HttpClientHandler on WASI now follows the mobile UseNativeHttpHandler pattern. The default is still WasiHttpHandler, and WasiApp.targets defaults UseNativeHttpHandler=true, so the SDK passes the switch to ILLink and SocketsHttpHandler is trimmed away. A trimmed hello-world HttpClient app keeps System.Net.Http.dll at ~289 KB by default, and ~631 KB with UseNativeHttpHandler=false.
  2. Tests. Enables the SocketsHttpHandler functional tests, System.Net.WebSockets.Tests and System.Net.WebSockets.Client.Tests on CoreCLR WASI.
    • TLS- and MD5-dependent tests are gated on new PlatformDetection.IsTlsSupported/IsMD5Supported capability probes rather than IsWasi, so they light up if either becomes available.
    • LoopbackProxyServer is now async and doesn't block in Dispose without threads.
    • Configuration.Certificates no longer fails fast when TLS isn't supported. Its PFX files can't be decrypted on WASI.
    • HTTP decompression tests now run on WASI too, including Brotli and Zstandard ([wasi] Enable Brotli and Zstandard on CoreCLR WASI #136257).

The WASI socket and event loop fixes these tests depend on were split out and have merged: zero-byte receives (#136394), event loop hook duplication and idle-socket spinning (#136395), and UDP ReceiveAsync (#136250).

Known gaps and follow-ups:

  • Mono WASI builds the same libraries but doesn't run these tests.
  • The functional tests use UseNativeHttpHandler=false, so they no longer exercise the default HttpClientHandler over WasiHttpHandler path on WASI.
  • A bound UDP socket still always subscribes to its outgoing stream, so it can keep the event loop busy.
  • A zero-byte receive can complete early once after its data is drained, because wasmtime keeps a socket readable until a read would block. wasmtime treats this as by design (wasmtime-wasi: Unexpected tcp stream break (Returning 0 for std::io::ErrorKind::WouldBlock) bytecodealliance/wasmtime#9667); the following real read waits correctly.
  • HttpClient.DefaultProxy on WASI now reads the standard proxy environment variables, as on Unix.

Validated locally on CoreCLR WASI (Release) with the System.Net.Http functional, System.Net.Sockets, System.Net.WebSockets and System.Net.WebSockets.Client tests, plus macOS runs of the changed shared test infrastructure.

Note

This PR description was generated with GitHub Copilot.

lewing and others added 4 commits October 21, 2026 13:27
Build the real SocketsHttpHandler for WASI on top of wasi:sockets, add a WASI
build of System.Net.WebSockets, and allow ClientWebSocket on WASI.

HttpClientHandler on WASI follows the mobile pattern: it uses the wasi:http
based WasiHttpHandler by default and SocketsHttpHandler when
System.Net.Http.UseNativeHttpHandler is false. WasiApp.targets defaults
UseNativeHttpHandler=true so the SDK passes the switch to the trimmer and the
unused handler is removed.

System.Net.Security has no WASI build, so the socket path doesn't configure
TLS options, and ClientWebSocket only touches SslOptions when a certificate
validation callback is set. Only http:// and ws:// work over SocketsHttpHandler.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Unix implements a zero-byte receive as a 1-byte MSG_PEEK, which WASI sockets
don't support, so the receive failed with OperationNotSupported. SocketsHttpHandler
relies on zero-byte reads for HTTP/2. On WASI, report that the receive would
block; the queued zero-byte operation completes once the socket is readable.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CheckPollables re-registered a poll hook once per pollable it subscribed, so a
connected socket (input + output pollables) doubled its hook entries every
iteration until memory ran out, and hooks with no pollables were dropped.
Re-register each live hook once.

A connected socket also always subscribed to its output stream, which is
almost always writable, so the event loop never blocked and spun with any idle
connection. Only subscribe to directions with a waiting operation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run the SocketsHttpHandler functional tests and the System.Net.WebSockets and
System.Net.WebSockets.Client tests on WASI. The test projects use
SocketsHttpHandler through HttpClientHandler (UseNativeHttpHandler=false), as on
Android.

Tests that need TLS or MD5 are gated on new PlatformDetection.IsTlsSupported and
IsMD5Supported capability probes, so they light up if those become available.
Tests that need APM, pipes, NetworkInformation, synchronous socket operations or
blocking waits are skipped on WASI.

Make shared test infrastructure work without TLS or threads:
- Configuration.Certificates only fails fast when TLS is supported, so an
  unloadable test certificate doesn't terminate the test process.
- LoopbackProxyServer uses async I/O and doesn't block in Dispose without
  multithreading.
- DefaultConnectCallback creates an address-family-specific socket on WASI,
  which doesn't support dual-mode sockets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 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.

lewing and others added 3 commits October 21, 2026 13:44
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Guard SslOptions use in ClientWebSocket on WASI (CA1416), suppress CA1416 for
the System.Net.Http wasi TFM where TLS paths throw PlatformNotSupportedException,
and narrow CreateNativeHandler's return type (CA1859).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing
lewing requested a balanced review from Copilot October 21, 2026 20:27
@lewing lewing added arch-wasm WebAssembly architecture os-wasi Related to WASI variant of arch-wasm labels Oct 21, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

@lewing
lewing requested a review from pavelsavara October 21, 2026 20:34

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

Source inspection confirms zero-byte receive correctness issues and missing TLS guards in newly enabled tests.

5 open issues
What changed in this PR

Enables socket-based HTTP and WebSockets on WASI while retaining WasiHttpHandler as the default.

Changes:

  • Adds WASI handler selection and WebSocket support.
  • Adjusts zero-byte receives and event-loop polling.
  • Enables networking tests with capability-based exclusions and asynchronous test infrastructure.

Validation here was limited to source inspection; no builds or tests were run.

File Description
src/​mono/​wasi/​build/​WasiApp.targets Defaults to the native HTTP handler.
src/​libraries/​tests.proj Enables WASI WebSocket test projects.
src/​libraries/​System.Private.CoreLib/​src/​System/​Threading/​Wasi/​WasiEventLoop.cs Preserves each live hook once.
src/​libraries/​System.Private.CoreLib/​src/​System/​Threading/​UnixHandleAsyncContext.Wasi.cs Limits connected TCP subscriptions to pending operations.
src/​libraries/​System.Private.CoreLib/​src/​System/​Threading/​UnixHandleAsyncContext.cs Exposes queue waiting state.
src/​libraries/​System.Net.WebSockets/​tests/​WebSocketStreamTests.cs Separates threading-dependent stream tests.
src/​libraries/​System.Net.WebSockets/​src/​System.Net.WebSockets.csproj Adds the WASI target.
src/​libraries/​System.Net.WebSockets.Client/​tests/​System.Net.WebSockets.Client.Tests.csproj Selects socket-based HTTP for WASI tests.
src/​libraries/​System.Net.WebSockets.Client/​tests/​ConnectTest.Invoker.cs Guards TLS-dependent invoker cases.
src/​libraries/​System.Net.WebSockets.Client/​tests/​ClientWebSocketTestBase.cs Guards certificate callback configuration.
src/​libraries/​System.Net.WebSockets.Client/​tests/​ClientWebSocketOptionsTests.cs Guards secure proxy cases.
src/​libraries/​System.Net.WebSockets.Client/​src/​System/​Net/​WebSockets/​WebSocketHandle.Managed.cs Avoids unsupported WASI TLS configuration.
src/​libraries/​System.Net.WebSockets.Client/​src/​System.Net.WebSockets.Client.csproj Removes the WASI exclusion.
src/​libraries/​System.Net.Sockets/​tests/​FunctionalTests/​SendReceive/​SendReceive.cs Enables zero-byte disconnect coverage.
src/​libraries/​System.Net.Sockets/​src/​System/​Net/​Sockets/​SocketPal.Unix.cs Adds WASI zero-byte receive handling.
src/​libraries/​System.Net.Http/​tests/​FunctionalTests/​System.Net.Http.Functional.Tests.csproj Selects SocketsHttpHandler on WASI.
src/​libraries/​System.Net.Http/​tests/​FunctionalTests/​SyncHttpHandlerTest.cs Updates synchronous-send skip reasons.
src/​libraries/​System.Net.Http/​tests/​FunctionalTests/​SocketsHttpHandlerTest.Http2ExtendedConnect.cs Guards TLS-dependent cases.
src/​libraries/​System.Net.Http/​tests/​FunctionalTests/​SocketsHttpHandlerTest.cs Enables WASI tests and adds capability guards.
src/​libraries/​System.Net.Http/​tests/​FunctionalTests/​SocketsHttpHandlerTest.Cancellation.cs Uses cleartext when TLS is unavailable.
src/​libraries/​System.Net.Http/​tests/​FunctionalTests/​MetricsTest.cs Guards TLS-dependent metrics tests.
src/​libraries/​System.Net.Http/​tests/​FunctionalTests/​HttpClientHandlerTestBase.SocketsHttpHandler.cs Adapts socket creation and TLS configuration.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​WasiHttpHandler/​SystemProxyInfo.Wasi.cs Removes the no-proxy implementation.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​SocketsHttpHandler/​SocketsHttpHandler.cs Reports support on WASI.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​HttpProtocolException.cs Includes HTTP/2 exception helpers on WASI.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​HttpClientHandler.Wasi.InvokeNativeHandler.cs Adds native-handler forwarding.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​HttpClientHandler.cs Removes WASI-specific branches from the shared implementation.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​HttpClientHandler.AnyMobile.cs Extends handler selection to WASI.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​Headers/​KnownHeaders.cs Clarifies the Alt-Svc comment only.
src/​libraries/​System.Net.Http/​src/​System/​Net/​Http/​GlobalHttpSettings.cs Enables HTTP/2 settings on WASI.
src/​libraries/​System.Net.Http/​src/​System.Net.Http.csproj Includes socket HTTP implementation and dependencies.
src/​libraries/​Common/​tests/​TestUtilities/​System/​PlatformDetection.cs Adds TLS and MD5 capability probes.
src/​libraries/​Common/​tests/​System/​Net/​Http/​TestHelper.cs Guards TLS configuration.
src/​libraries/​Common/​tests/​System/​Net/​Http/​LoopbackProxyServer.cs Uses asynchronous I/O and avoids threadless blocking disposal.
src/​libraries/​Common/​tests/​System/​Net/​Http/​HttpClientHandlerTest.Proxy.cs Gates TLS and MD5 scenarios.
src/​libraries/​Common/​tests/​System/​Net/​Http/​HttpClientHandlerTest.MaxResponseHeadersLength.cs Gates threading-dependent tests.
src/​libraries/​Common/​tests/​System/​Net/​Http/​HttpClientHandlerTest.cs Adds capability and unsupported-operation guards.
src/​libraries/​Common/​tests/​System/​Net/​Http/​HttpClientHandlerTest.Asynchrony.cs Gates synchronization-context tests.
src/​libraries/​Common/​tests/​System/​Net/​Configuration.Certificates.cs Limits fail-fast diagnostics to TLS-capable platforms.
src/​coreclr/​vm/​wasm/​wasi/​callhelpers-pinvoke.cpp Documents WebSocket compression consumers in comments.

Review effort: Balanced

Comment thread src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs Outdated
Comment thread src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs Outdated
Comment thread src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs Outdated
lewing and others added 2 commits October 21, 2026 15:59
Completing a queued zero-byte receive on any poll event mistook output
readiness for input, left receives on unconnected TCP sockets pending, and
completed UDP receives before a datagram arrived. Poll the socket for input
instead, and report the receive error when it can't be polled.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing

lewing commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

/azp run runtime-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@lewing

lewing commented Oct 21, 2026

Copy link
Copy Markdown
Member Author

I'm staging several follow-ups to this to add back wasihttphandler tests, enable more features and fix the things uncovered in the process.

@@ -0,0 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The file name confuses me. I would say that the WasiHttpHandler is "system" HTTP client, rather than "native". As in "system interface"

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.

this follows the mobile naming convention, native here is essentially "not managed/system" vs socketshttphandler which is the managed implementation on top of sockets

lewing added a commit that referenced this pull request Oct 23, 2026
Enables `System.IO.Compression.Brotli` and Zstandard (`ZstandardStream`
and related types) on CoreCLR WASI. Browser is unchanged and still
excludes both.

## Why

Both codecs were excluded on all of wasm because browser leaves them out
for download size. WASI apps always relink the native runtime with a
P/Invoke table generated from the app's trimmed assemblies, so a codec's
native code is only linked into apps that use it.

Measured on relinked, trimmed sample apps (Release, `wasm-tools strip
-a`):

| App | Host size | Delta | gzip -9 delta |
|---|---|---|---|
| GZip only | 5,521,734 | — | — |
| GZip + BrotliStream | 6,355,085 | +833 KB | +316 KB |
| GZip + ZstandardStream | 6,028,091 | +506 KB | +121 KB |

The GZip-only host contains no `Brotli*` or `ZSTD_*` symbols.

## Changes

- **Native/build:** brotli build, includes and entry points are now
gated on `!browser` instead of `!wasm`. The brotli libs are linked in
the CoreCLR WASI app relink, the NativeAOT WASI link and the Mono WASI
runtime link; libzstd is linked unconditionally in the CoreCLR relink.
Duplicate libs.native copies are excluded from the WASI CoreCLR runtime
pack, as for `libz.a`.
- **Managed:** `System.IO.Compression.Brotli` gains a `-wasi` TFM.
System.IO.Compression compiles the real Zstandard implementation for
wasi. `wasi` is removed from `UnsupportedOSPlatform` on the Brotli
assembly, the Zstandard types, and
`BrotliCompressedContent`/`ZstandardCompressedContent` (src and ref).
- **Call helpers:** `src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp`
is regenerated in the commit that adds each codec's P/Invokes
(Compression.Native table: 9 → 21 → 51 entries).
- **Tests:** the Brotli tests now target WASI, and
`CompressedContentTest.NonBrowser.cs` is compiled for WASI.

## Validation

CoreCLR WASI, Release, wasmtime 49.0.1:

- System.IO.Compression.Brotli.Tests: 259 run, 251 passed, 0 failed, 8
skipped (existing `IsMultithreadingSupported` conditions).
- System.Net.Http.Functional.Tests `-class CompressedContentTest`: 48
run, 47 passed, 1 skipped (existing 32-bit zstd `SmallestSize` case).
This covers the br and zstd round-trips.
- System.IO.Compression.Tests built for wasi locally: all 316 Zstandard
tests passed, 10 skipped on capability conditions.
- ApiCompat outer builds of Brotli, System.IO.Compression and
System.Net.Http are clean.
- The Mono WASI runtime (`mono.runtime+mono.corelib+mono.wasiruntime`)
builds and links.
- Not run locally: a NativeAOT WASI app link that uses Brotli.

## Known gaps

- There's no dedicated Zstandard test suite on WASI yet.
`System.IO.Compression.Tests` doesn't target wasi because its WinZip AES
and encrypted-zip tests fail there (cryptography isn't supported on
WASI) and need gating first.
- `SocketsHttpHandler` decompression of br/zstd on WASI is covered by
#136234.

Related: #91279 (Brotli on browser; not addressed here)

> [!NOTE]
> This PR description was generated with GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ttp-handler

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ttp-handler

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brotli and Zstandard now work on WASI (dotnet#136257). The decompression test
servers disposed compression streams synchronously over the connection, which
WASI doesn't support, and the Brotli buffer-limit test's write loop never
yielded to a single-threaded client. The new Alt-Svc test needs TLS.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing added a commit that referenced this pull request Oct 26, 2026
On Unix, a zero-byte receive is implemented as a 1-byte `MSG_PEEK`.
wasi-libc rejects `MSG_PEEK` with `EOPNOTSUPP`, so every zero-byte
receive fails on WASI. Callers such as HTTP/2 in `SocketsHttpHandler`
rely on zero-byte reads to wait for data.

This was split out of #136234 at a reviewer's request.

## Fix

On WASI, a zero-byte receive with no flags now does this instead:

- It polls the socket for `POLLIN` with a zero timeout, and completes
only when the socket is readable.
- If the socket can't be polled, it does a zero-length `recv` so the
real error is reported. For example, wasi-libc returns `EOPNOTSUPP` for
an unconnected TCP socket, and the `recv` reports `ENOTCONN` instead.
- WASI poll events don't say which direction is ready, so queued
zero-byte receives re-run this check. They no longer complete on any
event.

Non-WASI behavior is unchanged.

Not covered: a zero-byte `ReceiveFrom`/`ReceiveMessageFrom` that passes
an endpoint still takes the `MSG_PEEK` path on WASI.

## Known caveat

After data has been drained, one zero-byte receive can complete early,
because wasmtime keeps readiness set until a read would block. wasmtime
treats this as by design (bytecodealliance/wasmtime#9667). The next real
read then waits correctly.

## Validation

- **CoreCLR WASI, Release, wasmtime 49.0.1:** the full
System.Net.Sockets functional tests ran 653 tests: 565 passed, 0 failed,
88 skipped. The baseline on main is about 645 run with 0 failed. The two
new tests passed in all 4 variants that run on WASI.
- **macOS, Release:** I ran the `SendReceive_*` classes (Sync,
SyncForceNonBlocking, Apm, Task, Eap, SpanSync, MemoryArrayTask,
MemoryNativeTask). Total was 252 with 0 failed, and both new tests
passed in every variant.
- `Receive0Byte_NotConnected_Throws` runs on all platforms.
`Receive0Byte_Udp_CompletesWhenDatagramArrives` runs on Unix only,
because on Windows a zero-byte receive consumes the datagram.
- `SocketsHttpHandler`/HTTP/2 isn't enabled on WASI in main, so the
motivating HTTP/2 scenario is covered in #136234 rather than here.
- Building main locally needed the `fgopt.cpp` fix from #136388. That
fix is not part of this PR.

Related: #107981

> [!NOTE]
> This PR was generated with GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing added a commit that referenced this pull request Oct 26, 2026
)

Split out of #136234 at a reviewer's request.

Two bugs in the WASI event loop's socket poll hooks:

1. **Hook duplication.** `WasiEventLoop.CheckPollables` re-added each
poll hook once per pollable it subscribed, not once per hook. A
connected TCP socket subscribes two pollables (input and output), so its
hook doubled on every pass. Hooks that subscribed nothing were dropped
for good. The fix tracks the live hooks and re-adds each one once.
2. **Idle socket spinning.** For a connected TCP socket,
`BeforePollHook` always subscribed the output stream, which is almost
always writable, so any idle connection kept `poll` from blocking. It
now subscribes input or output only when `_readQueue` or `_writeQueue`
has a waiting operation (new `OperationQueue.IsWaiting`). The hook runs
before every poll, so operations enqueued later are picked up on the
next pass.

**Found** running System.Net.WebSockets.Client.Tests with
SocketsHttpHandler enabled on WASI (#136234). That run ran out of memory
inside the event loop. With the fix, the suite ran 391 tests with no
OOM, and idle CPU of the wasmtime process dropped from about 99% to 0%.
Main has no HTTP/2 or WebSockets client on WASI, so that exact repro
needs #136234.

**Validation** (CoreCLR WASI Release, wasmtime 49.0.1),
System.Net.Sockets functional tests: no new failures with the fix (one
extra candidate test was included in both runs and later removed).
- with the fix: 646 run, 558 passed, 0 failed, 88 skipped
- main: 646 run, 558 passed, 0 failed, 88 skipped

Instrumenting `CheckPollables` on main showed the hook count going 1 → 2
→ 7 → 124 → 31,744 within 16 passes while 4 connected sockets waited on
receives. The Sockets tests don't keep connections alive long enough to
OOM. Two candidate regression tests (idle TCP pairs with pending
receives, and a UDP receive before any datagram) both passed on main, so
no new test is included.

Known gap (follow-up): the UDP `BOUND_STREAMING`/`CONNECTED` path still
always subscribes the outgoing datagram stream.

> [!NOTE]
> This PR was generated with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ttp-handler

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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

arch-wasm WebAssembly architecture area-System.Net.Http os-wasi Related to WASI variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants