SimpleHttpListener.Rx
7.1.0
See the version list below for details.
dotnet add package SimpleHttpListener.Rx --version 7.1.0
NuGet\Install-Package SimpleHttpListener.Rx -Version 7.1.0
<PackageReference Include="SimpleHttpListener.Rx" Version="7.1.0" />
<PackageVersion Include="SimpleHttpListener.Rx" Version="7.1.0" />
<PackageReference Include="SimpleHttpListener.Rx" />
paket add SimpleHttpListener.Rx --version 7.1.0
#r "nuget: SimpleHttpListener.Rx, 7.1.0"
#:package SimpleHttpListener.Rx@7.1.0
#addin nuget:?package=SimpleHttpListener.Rx&version=7.1.0
#tool nuget:?package=SimpleHttpListener.Rx&version=7.1.0
SimpleHttpListener.Rx
An Rx-based HTTP listener for TCP, UDP, and UDP multicast traffic.
Please star this project if you find it useful. Thank you.
Overview
SimpleHttpListener.Rx is a .NET library for HTTP message handling over application-controlled transports. It can listen for HTTP over TCP and UDP, including UDP multicast, which makes it useful for protocols such as UPnP/SSDP.
The library is built with Reactive Extensions, exposing incoming HTTP messages as an IObservable<HttpRequestResponse> for asynchronous processing.
Version 7.0.0 is a modernization release: .NET 10, HttpMachine.PCL 6.0.0 span-based parsing, HTTP keep-alive with concurrent connection handling, and a cleaned-up public API. See Breaking changes in 7.0.0 if you are upgrading.
Usage
Turn a TcpListener or UdpClient into an observable HTTP listener with the ToHttpListenerObservable extension method.
Namespaces
using SimpleHttpListener.Rx;
using SimpleHttpListener.Rx.Model;
TCP HTTP listener
Avoid an async subscriber such as .Subscribe(async x => ...); it can make Rx error handling and scheduling difficult. Instead, convert the asynchronous operation with Observable.FromAsync, then concatenate it as shown below.
var tcpListener = new TcpListener(IPAddress.Loopback, 8088);
var cts = new CancellationTokenSource();
var disposable = tcpListener
.ToHttpListenerObservable(cts.Token)
.Do(r => Console.WriteLine($"{r.Method} {r.Path} from {r.RemoteEndPoint}"))
// Send reply to the client
.Select(r => Observable.FromAsync(() => r.SendResponseAsync(new HttpResponse
{
Headers =
{
["Date"] = DateTime.UtcNow.ToString("r"),
["Content-Type"] = "text/html; charset=UTF-8"
},
Body = Encoding.UTF8.GetBytes($"<html><body><h1>Hello, World! {DateTime.Now}</h1></body></html>")
})))
.Concat()
.Subscribe(
_ => Console.WriteLine("Reply sent."),
ex => Console.WriteLine($"Exception: {ex}"),
() => Console.WriteLine("Completed."));
The listener starts when the observable is first subscribed and stops when the last subscription is disposed (or the token is cancelled). Re-subscribing restarts it.
Keep-alive and connection ownership
New in 7.0.0: connections are handled concurrently, and an HTTP/1.1 keep-alive connection emits one message per request — a slow or idle client no longer blocks other clients.
Ownership of the underlying connection is simple:
- While the listener is reading a connection, the listener owns it. It disposes the connection when the client closes it or a fatal parse/IO error occurs.
- When a message with
ShouldKeepAlive == falseis emitted, the listener stops reading and the consumer owns the connection.SendResponseAsyncin auto mode (closeConnection: null, the default) closes it after the response is written, so the simple flow above handles both cases correctly. - Disposing a connection yourself while the listener is reading it is treated as a normal close, not an error.
SendResponseAsync always emits a correct Content-Length (including 0 for empty bodies) unless you set Content-Length or Transfer-Encoding yourself, and adds the appropriate Connection: close / Connection: keep-alive header automatically.
UDP HTTP listener
The following UDP listener receives UPnP SSDP multicast traffic from the local network.
var udpClient = new UdpClient
{
ExclusiveAddressUse = false,
MulticastLoopback = true
};
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
udpClient.JoinMulticastGroup(IPAddress.Parse("239.255.255.250"));
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 1900));
var disposable = udpClient
.ToHttpListenerObservable(cts.Token, ErrorCorrection.HeaderCompletionError)
.Subscribe(r =>
{
Console.WriteLine($"{r.Method} from {r.RemoteEndPoint}");
});
ErrorCorrection.HeaderCompletionError is optional. Some SSDP/UPnP devices send messages whose header section does not end with the required empty line (\r\n\r\n); with the correction enabled such messages still parse. Without it they are emitted with HasParsingErrors == true and IsEndOfMessage == false. UDP messages have Connection == null; replying is up to you (e.g. via UdpClient.SendAsync).
WebSockets (7.1.0+)
The listener accepts incoming WebSocket connections without ASP.NET/Kestrel. A WebSocket
handshake arrives as a normal HTTP request with IsUpgradeRequest == true; the listener stops
reading that connection and hands ownership to you. Call AcceptWebSocketAsync to complete the
RFC 6455 handshake and get a standard System.Net.WebSockets.WebSocket (all framing, ping/pong
and close handling comes from the .NET runtime):
var disposable = tcpListener
.ToHttpListenerObservable(cts.Token)
.Subscribe(request =>
{
if (request.IsUpgradeRequest)
{
_ = HandleWebSocketAsync(request);
}
else
{
_ = request.SendResponseAsync(new HttpResponse { Body = "Hello, World"u8.ToArray() });
}
});
static async Task HandleWebSocketAsync(HttpRequestResponse request)
{
try
{
using var webSocket = await request.AcceptWebSocketAsync();
// Standard WebSocket API from here: ReceiveAsync / SendAsync / CloseAsync…
}
finally
{
request.Connection?.Dispose(); // consumer owns the connection after the upgrade emission
}
}
Notes and limitations:
- After an upgrade request is emitted the listener no longer reads that connection — complete
the handshake or dispose
Connection, even if you reject the upgrade. ws://only: the listener runs on a rawTcpListenerwith no TLS, so browsers can only connect from non-HTTPS pages. This targets LAN/local/native-app scenarios.AcceptWebSocketAsyncvalidates the handshake (version 13, key present) and throwsInvalidOperationExceptionfor invalid requests; sending an error response is up to you.- Sub-protocols are pass-through (
subProtocolparameter), no negotiation logic. - For the client side of the story, see
WebsocketClientLite.PCL — the
samples/SimpleHttpListener.Rx.Sample.WebSocketClientproject connects it to the server sample's echo endpoint.
Parse errors
The listener observables never fail because of one bad client. Malformed input or a connection that closes mid-message produces an emission with HasParsingErrors == true, and the listener keeps serving other connections.
Breaking changes in 7.0.0
7.0.0 targets .NET 10 and ships one assembly: the separate ISimpleHttpListener.Rx interface assembly/package is gone, as are the IHttpCommon/IHttpHeaders/IParseControl/IHttpRequest/IHttpResponse/IHttpRequestReponse interfaces. Everything lives in the SimpleHttpListener.Rx and SimpleHttpListener.Rx.Model namespaces.
| v6 | v7 |
|---|---|
IHttpRequestReponse (misspelled) interface |
HttpRequestResponse class |
RequestType enum (TCP/UDP) |
HttpTransport enum (Tcp/Udp) |
MessageType (from HttpMachine) |
MessageType (own enum, Request/Response) |
ResponseReason |
ReasonPhrase |
LocalIpEndPoint / RemoteIpEndPoint |
LocalEndPoint / RemoteEndPoint |
MemoryStream Body |
ReadOnlyMemory<byte> Body |
TcpClient / ResponseStream properties |
IHttpConnection? Connection (Stream, endpoints, Dispose) |
IsEndOfRequest |
IsEndOfMessage |
IsUnableToParseHttp / IsRequestTimedOut |
HasParsingErrors |
UserContext, CancellationTokenSource |
removed |
Headers (uppercase keys, last duplicate wins) |
Headers (uppercase keys, case-insensitive lookup, duplicates comma-joined per RFC 9110 §5.2) |
new HttpSender().SendTcpResponseAsync(request, response) |
request.SendResponseAsync(response) extension |
HttpResponse.Body (MemoryStream) |
HttpResponse.Body (ReadOnlyMemory<byte>) |
uri.Host.GetIPv4Address() (sync) |
await uri.Host.GetIPv4AddressAsync() |
TcpClientEx / UriEx / HttpListenerEx classes |
TcpClientExtensions / UriExtensions / HttpListenerExtensions |
| One message per connection, sequential handling | Multiple messages per keep-alive connection, concurrent connections (emissions interleave) |
Behavior note: v6 closed every connection after one message. v7 honors keep-alive, so a consumer must respond per message; SendResponseAsync auto mode preserves v6-style close semantics whenever the client asked for Connection: close or HTTP/1.0.
HttpRequestResponse and HttpResponse are record types, so with expressions work for derived copies. Their equality is deliberately reference-based (a received message carries a live connection and a body buffer, so member-wise comparison would be misleading).
History
SimpleHttpListener.Rx is the successor to Simple HTTP Listener PCL. The legacy package remains available as SimpleHttpListener.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- HttpMachine.PCL (>= 6.0.1)
- System.Reactive (>= 7.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on SimpleHttpListener.Rx:
| Package | Downloads |
|---|---|
|
SSDP.UPnP.PCL
An Rx-based SSDP library for discovering and advertising UPnP Device Architecture 2.0 devices and services. |
|
|
UPnP.Rx
A modern, functional, Rx-based UPnP control point for .NET: discover devices, browse their services, call their actions — as observables and immutable records. Includes an IGD port-mapping client with auto-renewing leases. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on SimpleHttpListener.Rx:
| Repository | Stars |
|---|---|
|
uholeschak/ediabaslib
.NET BMW and VAG Ediabas interpreter library
|