In my previous article, I talked about implementing the HTTP protocol over a standard TCP socket. Today, I will be using few libraries available in python that implements HTTP and try to build something like Flask web framework.
What is Flask Web Framework
Flask is a lightweight and versatile web framework for Python, designed to make building web applications simple and easy. It provides the tools and libraries needed to develop web applications quickly and efficiently, with minimal boilerplate code. Flask follows the WSGI (Web Server Gateway Interface) standard, making it compatible with a wide range of web servers. Its simplicity, flexibility, and extensive documentation have made it a popular choice among developers for creating web applications, APIs, and more.
Here’s an example of Flask code that we try to create using inbuilt libraries in Python.
from flask import Flask, render_template
import os
PORT = os.getenv('PORT')
app = Flask(__name__)
@app.route('/', methods=["GET"])
def home():
return render_template('index.html', context={
"PORT": PORT
})
if __name__ == '__main__':
app.run(port=PORT)
The index.html
looks like this.
<!-- File - index.py -->
<h1>This is an HTTP server running of Port: {PORT}</h1>
HTTP Server class
Python provides basic functionalities to implement HTTP protocol under http.server
module. We will using this as our base for our version of Flask.
# File - myflask.py
import http.server as http_server
class MyFlask():
def __init__(self, http_handler=http_server.SimpleHTTPRequestHandler) -> None:
self.http_handler = http_handler
def run(self, host="localhost", port=3000):
httpd = http_server.HTTPServer((host, port), self.http_handler)
print(f"Running server at {host}:{port}")
httpd.serve_forever()
Now let’s create a server file that import as uses MyFlask
class.
# File - main.py
from myflask import MyFlask
app = MyFlask()
if __name__ == "__main__":
app.run()
If you run main.py at this point, and open http://localhost:3000 you can see index.html
getting rendered.
python3 main.py
Here SimpleHTTPRequestHandler
simply rendered the current working directory and since index.html
happens to be there, the server simply returned the contents of that file. If we rename index.html to something else then an Index Of
page will be rendered.
Building Routes
What we have built works, but that only provides a simple static website server with no added security. We can change this behavior by implementing our own https handler. we can do this simply by defining a Http Request Handler class and passing it through to the MyFlask
constructor
# File - main.py
from myflask import MyFlask
app = MyFlask(MyHttpRequestHandler)
if __name__ == "__main__":
app.run()
Now let’s define this MyHttpRequestHandler
class.
Structure of HTTP Request Handler
To implement a custom HTTP request handler from scratch, you would typically subclass the SimpleHTTPRequestHandler
class from the http.server
module. This allows you to define custom behavior for handling different types of HTTP requests, such as GET, POST, PUT, DELETE, etc.
You can handle more HTTP methods by implementing do_{method}
method inside MyHttpRequestHandler
class. For example, do_POST
, do_DELETE
, do_PATCH
, etc.
# File - main.py
from myflask import MyFlask
from http.server import SimpleHTTPRequestHandler
class MyHttpRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
self.send_error(418)
app = MyFlask(MyHttpRequestHandler)
if __name__ == "__main__":
app.run()
The problem with this implementation is that the server will return HTTP error 418 no matter the page you try to visit.
We can easily implement routes by performing some pattern matching over the self.path
.
# File - main.py
from myflask import MyFlask
from http.server import SimpleHTTPRequestHandler
class MyHttpRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
self.wfile.write("<p>Hello World</p>".encode()) # HTTP body
app = MyFlask(MyHttpRequestHandler)
if __name__ == "__main__":
app.run()
You might find the order I wrote the command inside do_GET
kind of similar to the HTTP Response format we discussed in our Create HTTP server from Scratch article.
HTTP/1.1 200 OK
Content-Type text/html
<p>Hello World</p>
First we sent the response code which is 200
in this case. Then we list all our headers, in this case just Content-Type
and once we listed all the required headers we mark the end of headers using self.end_headers()
and finally we write the body and encoded it as bytes.
If you want to support another page, we can simply add another pattern matching.
# File - main.py
from myflask import MyFlask
from http.server import SimpleHTTPRequestHandler
class MyHttpRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
self.wfile.write("<p>Hello World</p>".encode()) # HTTP body
elif self.path == "/page1":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
self.wfile.write("<p>Rendering Page 1</p>".encode()) # HTTP body
app = MyFlask(MyHttpRequestHandler)
if __name__ == "__main__":
app.run()
Handling HTML template expressions
Now finally let’s do some context filling something like we do in Flask.
# File - main.py
from myflask import MyFlask
from http.server import SimpleHTTPRequestHandler
import os
PORT = (os.getenv('PORT') or 3000) # port 3000 by default
class MyHttpRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
self.wfile.write("<p>Hello World</p>".encode()) # HTTP body
elif self.path == "/page1":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
self.wfile.write("<p>Rendering Page 1</p>".encode()) # HTTP body
elif self.path == "/port":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
res_body = open('index.html',"r").read().format_map({
"PORT":PORT
})
self.wfile.write(res_body.encode()) # HTTP body
app = MyFlask(MyHttpRequestHandler)
if __name__ == "__main__":
app.run(port=PORT)
Managing Error codes
Final missing piece in our HTTP server puzzle is managing different error codes. In SimpleHTTPRequestHandler
there is a method called send_error()
this is used send an error code to the client. We have used this method in the beginning before we implemented the route.
# File - main.py
from myflask import MyFlask
from http.server import SimpleHTTPRequestHandler
import os
PORT = (os.getenv('PORT') or 3000) # port 3000 by default
class MyHttpRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
self.wfile.write("<p>Hello World</p>".encode()) # HTTP body
elif self.path == "/page1":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
self.wfile.write("<p>Rendering Page 1</p>".encode()) # HTTP body
elif self.path == "/port":
self.send_response(200) # Send HTTP code
self.send_header("Content-Type", "text/html") # Send Headers
self.end_headers() # Mark the end of Headers
res_body = open('index.html',"r").read().format_map({
"PORT":PORT
})
self.wfile.write(res_body.encode()) # HTTP body
else:
self.send_error(404)
app = MyFlask(MyHttpRequestHandler)
if __name__ == "__main__":
app.run(port=PORT)
Conclusion
In conclusion, while implementing a custom HTTP request handler can provide valuable insight into how web servers operate and how frameworks like Flask, Jinja, or Django function under the hood, for most practical purposes, using the SimpleHTTPRequestHandler
provided by the http.server
module is often the better choice.
The SimpleHTTPRequestHandler
is ideal for testing, prototyping, and learning purposes due to its simplicity and ease of use. It allows for quick setup of a basic HTTP server that can serve static files and handle simple GET requests. Moreover, it provides a straightforward platform for experimenting with web development concepts without the added complexity of implementing a custom request handler.
However, it's important to note that while the SimpleHTTPRequestHandler
is suitable for testing and learning, it is not the optimal solution for a production-ready server. Production environments typically require more advanced features such as security, scalability, performance optimization, and support for various HTTP methods beyond just GET requests. In such cases, using a mature and feature-rich web framework like Flask, Jinja, or Django, which are specifically designed for building robust web applications, would be the recommended approach.