C# HttpListener
last modified October 16, 2026
In this article we show how to create simple HTTP servers in C# with the
HttpListener class.
The Hypertext Transfer Protocol (HTTP) is an application protocol for distributed, collaborative, hypermedia information systems. HTTP is the foundation of data communication for the World Wide Web.
An HTTP server uses HTTP to respond to client requests made by browsers, crawlers, or tools such as curl or wget. It responds with a web resource (such as an HTML page, an image, or JSON data) or an HTTP error message.
HttpListener is a simple, programmatically controlled HTTP protocol
listener. It is located in the System.Net namespace. It is useful
for small tools, tests, and learning how HTTP works. Production web applications
and APIs are built with ASP.NET Core, which uses the much faster and more
capable Kestrel server.
localhost requires administrator rights or a URL reservation
created with netsh http add urlacl. On Linux and macOS, no special
setup is needed.
The servers in this article run until they are stopped with
Ctrl+C. We test them with the curl tool from
another terminal.
C# HttpListener status
In the first example, the server responds with a status code. HTTP response status codes indicate whether a specific HTTP request has been successfully completed.
using System.Net;
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
Console.WriteLine("Listening on port 8001...");
while (true)
{
HttpListenerContext ctx = listener.GetContext();
using HttpListenerResponse resp = ctx.Response;
resp.StatusCode = (int) HttpStatusCode.OK;
resp.StatusDescription = "Status OK";
}
In the example, the listener responds with HttpStatusCode.OK.
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
We create an instance of HttpListener and add a URL prefix that
the listener handles. A prefix must end with a slash. It defines the scheme,
the host, the port, and the path; this prefix matches all paths.
listener.Start();
With Start, we begin to receive requests.
HttpListenerContext ctx = listener.GetContext(); using HttpListenerResponse resp = ctx.Response;
Inside the loop, we call GetContext, which waits for an incoming
request and returns when one is received. The context gives us the request and
the response objects. Disposing the response sends it to the client.
resp.StatusCode = (int) HttpStatusCode.OK; resp.StatusDescription = "Status OK";
We set the status code and the description of the response.
$ curl -i localhost:8001 HTTP/1.1 200 Status OK Server: Microsoft-NetCore/2.0 Date: Fri, 16 Oct 2026 17:09:33 GMT Transfer-Encoding: chunked
With the -i option, curl prints the response headers.
C# HttpListener send text
The next example sends text data to the client.
using System.Net;
using System.Text;
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
Console.WriteLine("Listening on port 8001...");
while (true)
{
HttpListenerContext ctx = listener.GetContext();
HttpListenerRequest req = ctx.Request;
Console.WriteLine($"Received request for {req.Url}");
using HttpListenerResponse resp = ctx.Response;
resp.ContentType = "text/plain; charset=utf-8";
byte[] buffer = Encoding.UTF8.GetBytes("Hello there!");
resp.ContentLength64 = buffer.Length;
using Stream ros = resp.OutputStream;
ros.Write(buffer, 0, buffer.Length);
}
The server sends a text message.
HttpListenerRequest req = ctx.Request;
Console.WriteLine($"Received request for {req.Url}");
On the server side, we log the request URL.
resp.ContentType = "text/plain; charset=utf-8";
The Content-Type header tells the client what kind of data to
expect and how the text is encoded. Headers and the status code must be set
before we write to the output stream.
byte[] buffer = Encoding.UTF8.GetBytes("Hello there!");
resp.ContentLength64 = buffer.Length;
We transform the message into bytes and set the content length.
using Stream ros = resp.OutputStream; ros.Write(buffer, 0, buffer.Length);
Finally, we write the bytes to the output stream.
$ curl localhost:8001 Hello there! $ curl localhost:8001/about?id=1 Hello there!
The server answers every path in the same way. It logs the requests:
Listening on port 8001... Received request for http://localhost:8001/ Received request for http://localhost:8001/about?id=1
C# HttpListener user agent
The User-Agent request header is a string that lets servers and network peers identify the application, operating system, vendor, and/or version of the requesting user agent.
using System.Net;
using System.Text;
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
Console.WriteLine("Listening on port 8001...");
while (true)
{
HttpListenerContext ctx = listener.GetContext();
HttpListenerRequest req = ctx.Request;
string data = req.UserAgent ?? "unknown";
using HttpListenerResponse resp = ctx.Response;
resp.ContentType = "text/plain; charset=utf-8";
byte[] buffer = Encoding.UTF8.GetBytes(data);
resp.ContentLength64 = buffer.Length;
using Stream ros = resp.OutputStream;
ros.Write(buffer, 0, buffer.Length);
}
The listener retrieves the User-Agent header from the client request and sends it back.
string data = req.UserAgent ?? "unknown";
The UserAgent property returns the value of the header, or
null if the client did not send it. Other headers are available in
the Headers collection.
$ curl localhost:8001 curl/7.81.0 $ curl -A "C# program" localhost:8001 C# program
The -A option of curl sets the User-Agent header.
C# HttpListener JSON response
In the next example, the server reads a query parameter and responds with JSON data.
using System.Net;
using System.Text.Json;
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
Console.WriteLine("Listening on port 8001...");
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
while (true)
{
HttpListenerContext ctx = listener.GetContext();
HttpListenerRequest req = ctx.Request;
string name = req.QueryString["name"] ?? "guest";
var greeting = new Greeting($"Hello, {name}!", name.Length);
using HttpListenerResponse resp = ctx.Response;
resp.ContentType = "application/json";
byte[] buffer = JsonSerializer.SerializeToUtf8Bytes(greeting, options);
resp.ContentLength64 = buffer.Length;
resp.OutputStream.Write(buffer);
}
record Greeting(string Message, int NameLength);
The QueryString property of the request contains the parsed and
decoded query parameters.
byte[] buffer = JsonSerializer.SerializeToUtf8Bytes(greeting, options);
The SerializeToUtf8Bytes method serializes the record directly
into UTF-8 bytes. The web defaults produce camel-case property names, which are
common in JSON APIs.
$ curl localhost:8001
{"message":"Hello, guest!","nameLength":5}
$ curl "localhost:8001?name=Jane"
{"message":"Hello, Jane!","nameLength":4}
C# HttpListener POST request
The body of a request is read from the InputStream property. In
the following example, the server accepts a user in JSON format.
using System.Net;
using System.Text;
using System.Text.Json;
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
Console.WriteLine("Listening on port 8001...");
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
while (true)
{
HttpListenerContext ctx = listener.GetContext();
HttpListenerRequest req = ctx.Request;
using HttpListenerResponse resp = ctx.Response;
if (req.HttpMethod != "POST")
{
resp.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
resp.AddHeader("Allow", "POST");
continue;
}
using var reader = new StreamReader(req.InputStream, req.ContentEncoding);
string body = reader.ReadToEnd();
User? user;
try
{
user = JsonSerializer.Deserialize<User>(body, options);
}
catch (JsonException)
{
user = null;
}
string reply;
if (user is null)
{
resp.StatusCode = (int) HttpStatusCode.BadRequest;
reply = "invalid JSON";
}
else
{
resp.StatusCode = (int) HttpStatusCode.Created;
reply = $"user {user.Name} ({user.Occupation}) created";
}
byte[] buffer = Encoding.UTF8.GetBytes(reply);
resp.ContentType = "text/plain; charset=utf-8";
resp.ContentLength64 = buffer.Length;
resp.OutputStream.Write(buffer);
}
record User(string Name, string Occupation);
The server accepts only POST requests. It deserializes the body and responds with the 201 Created status code, or with 400 Bad Request if the body is not valid JSON.
if (req.HttpMethod != "POST")
{
resp.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
resp.AddHeader("Allow", "POST");
continue;
}
For other methods, we respond with 405 Method Not Allowed and list the allowed
methods in the Allow header. The continue statement
skips to the next request; the response is sent when the using
declaration disposes it.
using var reader = new StreamReader(req.InputStream, req.ContentEncoding); string body = reader.ReadToEnd();
We read the whole body as text.
$ curl -i localhost:8001
HTTP/1.1 405 Method Not Allowed
Allow: POST
...
$ curl -d '{"name": "John Doe", "occupation": "gardener"}' localhost:8001
user John Doe (gardener) created
$ curl -d 'oops' localhost:8001
invalid JSON
With the -d option, curl sends a POST request with the given
body.
C# HttpListener send image
In the next example, the server sends an image or a 404 Not Found error, depending on the requested path.
using System.Net;
using System.Text;
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
Console.WriteLine("Listening on port 8001...");
while (true)
{
HttpListenerContext ctx = listener.GetContext();
string? path = ctx.Request.Url?.LocalPath;
if (path == "/image")
{
SendImage(ctx);
}
else
{
NotFound(ctx);
}
}
void NotFound(HttpListenerContext ctx)
{
using HttpListenerResponse resp = ctx.Response;
resp.StatusCode = (int) HttpStatusCode.NotFound;
resp.ContentType = "text/plain; charset=utf-8";
byte[] buf = Encoding.UTF8.GetBytes("404 - not found");
resp.ContentLength64 = buf.Length;
resp.OutputStream.Write(buf);
}
void SendImage(HttpListenerContext ctx)
{
using HttpListenerResponse resp = ctx.Response;
byte[] buf = File.ReadAllBytes("public/img/sid.png");
resp.ContentType = "image/png";
resp.ContentLength64 = buf.Length;
resp.OutputStream.Write(buf);
}
Inside the public/img directory of the project, we have a PNG
file.
string? path = ctx.Request.Url?.LocalPath;
From the request URL, we get the path without the query string.
void NotFound(HttpListenerContext ctx)
{
using HttpListenerResponse resp = ctx.Response;
resp.StatusCode = (int) HttpStatusCode.NotFound;
The NotFound function sends the 404 error message back to the
client. It is a standard HTTP error indicating that the resource was not
found.
byte[] buf = File.ReadAllBytes("public/img/sid.png");
resp.ContentType = "image/png";
Inside SendImage, we read the image with
File.ReadAllBytes and set the image/png content type.
$ curl -s -o sid.png -w "%{http_code} %{content_type} %{size_download}\n" localhost:8001/image
200 image/png 8090
$ curl -i localhost:8001/about
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Server: Microsoft-NetCore/2.0
Date: Fri, 16 Oct 2026 17:10:16 GMT
Content-Length: 15
404 - not found
We save the image to a file and print the status code, the content type, and the size of the response.
C# HttpListener asynchronous server
The previous servers handle one request at a time; a slow request blocks all the others. The following server handles requests asynchronously and stops cleanly when we press Ctrl+C.
using System.Net;
using System.Text;
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
cts.Cancel();
};
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
Console.WriteLine("Listening on port 8001; press Ctrl+C to stop");
cts.Token.Register(listener.Stop);
try
{
while (!cts.IsCancellationRequested)
{
HttpListenerContext ctx = await listener.GetContextAsync();
_ = HandleAsync(ctx);
}
}
catch (HttpListenerException) when (cts.IsCancellationRequested)
{
}
catch (ObjectDisposedException) when (cts.IsCancellationRequested)
{
}
Console.WriteLine("Server stopped");
static async Task HandleAsync(HttpListenerContext ctx)
{
using HttpListenerResponse resp = ctx.Response;
await Task.Delay(1000);
byte[] buf = Encoding.UTF8.GetBytes($"Handled {ctx.Request.Url?.LocalPath}\n");
resp.ContentType = "text/plain; charset=utf-8";
resp.ContentLength64 = buf.Length;
await resp.OutputStream.WriteAsync(buf);
}
Each request takes one second to process; this simulates slow work, such as a database query.
HttpListenerContext ctx = await listener.GetContextAsync(); _ = HandleAsync(ctx);
The GetContextAsync method waits for a request without blocking a
thread. We start the handler and immediately wait for the next request; we do
not await the handler. The discard _ shows that ignoring the task
is intentional.
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
cts.Cancel();
};
When the user presses Ctrl+C, we cancel the default termination of the process and signal the cancellation instead.
cts.Token.Register(listener.Stop);
On cancellation, the listener is stopped. The pending
GetContextAsync call then throws an exception, which we catch
only if the cancellation was requested.
$ time (curl localhost:8001/a & curl localhost:8001/b & curl localhost:8001/c & wait) Handled /c Handled /a Handled /b real 0m1.022s user 0m0.006s sys 0m0.017s
Three requests sent at the same time are handled in about one second, not three.
Source
In this article we have used HttpListener to create simple HTTP
servers.
Author
List all C# tutorials.