Web application from scratch, Part III
This is the third post in my web app from scratch series. If you haven’t read them yet, you can find the first part here and the second part here. You’ll want to read them first.
This part is going to be short and sweet. We’re going to cover request handlers and middleware. Here’s the source for part 2 so you can follow along. Let’s get to it!
Handlers
Last time we implemented all our request handling logic inside the
HTTPWorker
class. That’s not an appropriate place for application
logic to live in so we need to update that code to run arbitrary
application code that it knows nothing about. To do this, we’re going
to introduce the concept of a request handler. In our case a request
handler is going to be a function that takes in a Request
object and
returns a Response
object. Expressed as a type, that looks like
this:
|
|
Let’s modify our HTTPServer
so that it stores a set of request
handlers, each one assigned to a particular path prefix so that we can
host different applications at different paths. In HTTPServer
’s
constructor, let’s assign an empty list to the handlers
instance
variable.
|
|
Next, let’s add a method that we can use to add handlers to the
handler list. Call it mount
.
|
|
Now we need to update the HTTPWorker
class to take advantage of
these handlers. We need to make the workers’ constructor take the
handlers list as a parameter.
|
|
And then we need to update the handle_client
method to delegate
request handling to the handler functions. If none of the handlers
match the current path, then we’ll return a 404 and if one of the
handlers raises an exception then we’ll return a 500 error to the
client.
|
|
Lastly, we have to make sure we pass the handler list to the
HTTPWorker
s when we instantiate them in serve_forever
.
|
|
Now, whenever an HTTPWorker
receives a new connection, it’ll parse
the request and try to find a request handler to process it with.
Before the request is passed to a request handler, we remove the
prefix from its path property so that request handlers don’t have to
be aware of what prefix they’re running under. This’ll come in handy
when we write a handler that serves static files.
Since we haven’t mounted any request handlers yet, our server will reply with a 404 to any incoming request.
|
|
Let’s mount a request handler that always returns the same response.
|
|
Whatever path we visit now, we’ll get the same Hello!
response.
Let’s mount another handler to serve static files from a local folder.
To do this, we’re going to update our old serve_file
function and
turn it into a function that takes the path to some folder on disk and
returns a request handler that can serve files from that folder.
|
|
Finally, we’re going to call serve static and mount the result under “/static” before we mount our application handler.
|
|
All requests that begin with "/static"
will now be handled by the
generated static file handler and everything else will be handled by
the app handler.
Middleware
Given that our request handlers are plain functions that take a request and return a response, writing middleware – arbitrary functionality that can run before or after every request – is pretty straightforward: any function that takes a request handler as input and itself generates a request handler is a middleware.
Here’s how we might write a middleware that ensures that all incoming
requests have a valid Authorization
header:
|
|
To use it, we just pass it the app handler and mount the result.
|
|
Now all requests to the root handler will have to contain an authorization header with our super secret hard-coded value, otherwise they’ll get back a 403 response.
Winding down
That’s it for part 3. In part 4 we’re going to cover extracting an
Application
abstraction and implementing request routing. If you’d
like to check out the full source code and follow along, you can find
it here.
See ya next time!