ZetCode

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.

Note: On Windows, listening on addresses other than 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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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

HttpListener class

In this article we have used HttpListener to create simple HTTP servers.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.