Using HTTPListener to build a HTTP Server in C#

Nick Charlton

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, 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 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

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();
        }
}

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.

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!)

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, which brings a loop to the end and lets the HTTPServer tidy up after itself:

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:

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:

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, with a few adjustments.