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