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();
}
}
- I started with trying to use a
Thread
to handle the server, but struggled to getHTTPListener
to finish cleanly, here we use anAsyncCallback
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
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.