---
title: Using HTTPListener to build a HTTP Server in C#
teaser: 'I needed an HTTP server to test out a software integration on Windows. Nothing
  was quite basic enough to just echo out the responses, so I wrote one.

  '
tags: csharp,windows,http
author: Nick Charlton
published_on: 2022-04-29
---

In building an integration with some other client software, I needed something
I could easily put on a remote computer which could listen over HTTP and tell
me what requests were being made. We were getting responses back from certain
events using [webhooks][7], but this was hard to debug without having a basic
HTTP server to use. So I wrote one.

The software needed to run on Windows, so C# and .NET seemed a good choice.
C# has an [`HTTPListener` class][1] which handles most of the work for us,
but the example is not helpful enough alone to put something together which
would accept and report requests, then return a basic response.

## Basic `HTTPServer` Class

```csharp
using System;
using System.Net;
using System.IO;

public class HttpServer
{
    public int Port = 8080;

    private HttpListener _listener;

    public void Start()
    {
        _listener = new HttpListener();
        _listener.Prefixes.Add("http://*:" + Port.ToString() + "/");
        _listener.Start();
        Receive();
    }

    public void Stop()
    {
        _listener.Stop();
    }

    private void Receive()
    {
        _listener.BeginGetContext(new AsyncCallback(ListenerCallback), _listener);
    }

    private void ListenerCallback(IAsyncResult result)
    {
        if (_listener.IsListening)
        {
            var context = _listener.EndGetContext(result);
            var request = context.Request;

            // do something with the request
            Console.WriteLine($"{request.Url}");

            Receive();
        }
}
```

* I started with trying to use a [`Thread`][2] to handle the server, but
  struggled to get `HTTPListener` to finish cleanly, here we use an
  [`AsyncCallback`][3] instead which saves us from interacting directly with
  threads at all,
* Setting the prefixes on the `HTTPListener` sets the full URL which it will
  listen on. [You might need to explicitly allow this to enable it to run,
  otherwise you'll get an "Access Denied" error when trying to start it][5]

## Reporting on Queries

When receiving queries, we want to print out everything important, including
the HTTP method, full URL (including query string) and any body sent with it.
Any form parameters are sent inside the body, which is helpful for this
particular problem.

```csharp
Console.WriteLine($"{request.HttpMethod} {request.Url}");

if (request.HasEntityBody)
{
    var body = request.InputStream;
    var encoding = request.ContentEncoding;
    var reader = new StreamReader(body, encoding);
    if (request.ContentType != null)
    {
        Console.WriteLine("Client data content type {0}", request.ContentType);
    }
    Console.WriteLine("Client data content length {0}", request.ContentLength64);

    Console.WriteLine("Start of data:");
    string s = reader.ReadToEnd();
    Console.WriteLine(s);
    Console.WriteLine("End of data:");
    reader.Close();
    body.Close();
}
```

## Providing a Response

We're provided a `Stream` as part of the `Response` object. For this use case,
I didn't want to return anything apart from a `200 OK` response, so we set the
status code, give it a `text/plain` content type and write an empty byte array
before closing the stream. (If you don't close the stream, the request will
never complete!)

```csharp
var response = context.Response;
response.StatusCode = (int) HttpStatusCode.OK;
response.ContentType = "text/plain";
response.OutputStream.Write(new byte[] {}, 0, 0);
response.OutputStream.Close();
```

## As a Console Tool

This didn't need any complex UI, but I did want the console to behave well and
tidy up after itself. To do this, I [implemented something to intercept the
Ctrl+C interrupt][4], which brings a loop to the end and lets the `HTTPServer`
tidy up after itself:

```csharp
class Program
{
    private static bool _keepRunning = true;

    static void Main(string[] args)
    {
        Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e)
        {
            e.Cancel = true;
            Program._keepRunning = false;
        };

        Console.WriteLine("Starting HTTP listener...");

        var httpServer = new HttpServer();
        httpServer.Start();

        while (Program._keepRunning) { }

        httpServer.Stop();

        Console.WriteLine("Exiting gracefully...");
    }
}
```

## Using with PowerShell

From the client:

```powershell
PS > Invoke-WebRequest -URI http://127.0.0.1:8080/

StatusCode        : 200
StatusDescription : OK
Content           :
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Server: Microsoft-HTTPAPI/2.0
                    Date: Fri, 22 Apr 2022 17:31:16 GMT
                    Content-Type: text/plain


Headers           : {[Transfer-Encoding, System.String[]], [Server, System.String[]], [Date, System.String[]], [Content-Type,
                    System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 0
RelationLink      : {}
```

On the server:

```
Starting HTTP listener...
GET http://127.0.0.1:8080/
```

From the client:

```powershell
PS > Invoke-WebRequest -URI http://127.0.0.1:8080/ -Form @{ "name" = "Bob"; "email" = "bob@example.com" }

StatusCode        : 200
StatusDescription : OK
Content           :
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Server: Microsoft-HTTPAPI/2.0
                    Date: Fri, 22 Apr 2022 17:29:00 GMT
                    Content-Type: text/plain


Headers           : {[Transfer-Encoding, System.String[]], [Server, System.String[]], [Date, System.String[]], [Content-Type,
                    System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 0
RelationLink      : {}
```

On the server:

```
Starting HTTP listener...
GET http://127.0.0.1:8080/
Client data content type multipart/form-data; boundary="75dede48-bf92-47ce-b964-ab5b1d6cdee5"
Client data content length 321
Start of data:
--75dede48-bf92-47ce-b964-ab5b1d6cdee5
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name="name"

Bob
--75dede48-bf92-47ce-b964-ab5b1d6cdee5
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name="email"

bob@example.com
--75dede48-bf92-47ce-b964-ab5b1d6cdee5--

End of data:
```

_Apart from the links throughout this post, [this old forum post got me most of
the way there][6], with a few adjustments._

[1]: https://docs.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-6.0
[2]: https://docs.microsoft.com/en-us/dotnet/standard/threading/threads-and-threading
[3]: https://docs.microsoft.com/en-us/dotnet/api/system.asynccallback?view=net-6.0
[4]: https://stackoverflow.com/questions/177856/how-do-i-trap-ctrl-c-sigint-in-a-c-sharp-console-app
[5]: https://stackoverflow.com/questions/4019466/httplistener-access-denied
[6]: https://www.dreamincode.net/forums/topic/215467-c%23-httplistener-and-threading/
[7]: https://en.wikipedia.org/wiki/Webhook
