Disposable Web Servers: Solve it, then Toss it

Klaas van Schelven
Klaas van Schelven; October 18 - 4 min read
Some guy tossing servers in the garbage

Have you ever written your own web server? You should – it’s easier than you think, and it won’t take more than half an hour.

I’m not talking about building everything from scratch (that will take more time). Instead, I mean putting together something quick using ready-made parts.

But why would you want to do that?

What if you have a very specific problem in mind, and are not sure how to solve it by configuring an existing server? Or what if you suspect your server is actually part of the problem?

Why not just build a new server from scratch, solve the problem, and then throw it away? Here are a few real examples.

What the F… .py

Problem: you have a client that’s sending data to your server, but this data mysteriously disappears. Other servers are receiving the data just fine, so you suspect the problem is on your end.

In my case, I had a mysterious problem with Bugsink’s event handling for the NodeJS client: I got an exception in the parsing of event envelopes, which I quickly traced back to request.read() returning an empty result, i.e. b""

This led me to suspect a client-side error, and way too much time debugging on that side. Way too much, because that client was used against other servers without any problems.

So after a while, I accepted what perhaps should have been obvious from the start: the problem was on the server side. This raised the question: if Django is not receiving the data, and the client is probably sending it, where did it go?

Time to write a server to find out:

# a small "webserver" that just listens on 8000 and prints whatever it gets
# (raw data) on the console
import socket


def main():
    # create a socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # bind the socket to the address and port
    s.bind(('127.0.0.1', 8000))

    # start listening
    s.listen(1)
    print("Listening on port 8000")

    # accept a connection
    conn, addr = s.accept()
    print("Connection from", addr)

    # read the data
    data = conn.recv(9999999)
    print(data.decode())

    # close the connection
    conn.close()


if __name__ == "__main__":
    main()

How does this one work? It’s a simple server that listens on port 8000, accepts a connection, reads the data, and prints it to the console. Most importantly, it doesn’t do anything else. It doesn’t parse the data nor does it send a response. Oh, and it dies after the first request.

It does so by using the socket module, which is a low-level interface to the network. The implementation is basically: the first page from the Python socket documentation.

Using this server, I was quickly able to see that the data was indeed being sent by the client. Client-side problems having been ruled out, and Django clearly showing b"" for request.read() finally pushed my thinking in the direction of “there’s probably a problem with my toolset”.

Because I now had a full request (including headers) on screen, I was quickly able to zoom in on the problem: Django silently fails for Transfer-Encoding: Chunked. Quite a surprise, and not as easy to find without our little disposable server.

Fragile by design

Problem: you have a Django application that’s “stuck”, and you want to know why. By stuck, I mean that there is some code executing that is taking a long time. Wouldn’t it be nice to know where it’s stuck?

Simple: just press CTRL+C to send SIGINT to the process, and you’ll get a traceback right at the point where the application was interrupted: when an app is stuck, it’s usually because it spends a lot of time on a small part of the code, and the SIGINT will typically just make the program “go boom” right at that point.

Right? That’s how it works in any normal Python application. But not so in Django, because Django has fancy signal handling that does “what you want” for the SIGINT case. For the development server “what you want” is defined as “exiting without barfing over the screen” and for production servers it’s defined as “giving the individual worker processes some time to exit.

So let’s just build our own server, which is “crappy by design”. I’ll call it cornless to signify it’s quite the opposite of a [g]unicorn.

# A super-simple utility to run the WSGI application using the built-in WSGI
# server. The usefulness of this script is precisely because there is _no_
# special handling of SIGINT. This means that SIGINT can be used to get a
# traceback from the application as it was when interrupted. This is useful
# to answer the question "why is this application stuck?"

import sys

from wsgiref import simple_server
from YOURAPP.wsgi import application

if __name__ == "__main__":
    if len(sys.argv) < 2:
        host = "127.0.0.1"
        port = 8000
    else:
        host = sys.argv[1].split(":")[0]
        port = int(sys.argv[1].split(":")[1])

    httpd = simple_server.make_server("", port, application)
    httpd.serve_forever()

(replace YOURAPP with the name of your Django application)

This server is basically wsgiref.simple_server with a few utilities to be able to call it from the command line.

Conclusion

As you can (hopefully) see, these servers are not production-ready. They are not even development-ready. They are just a quick way to get a server up and running to solve a specific problem. Still, that’s sometimes all you need. And it might be a gateway drug to actually writing stuff from scratch.

Now that you’re here: Bugsink is a simple, self-hosted error tracking tool that’s API-compatible with Sentry’s SDKs. Check it out