Python HTTP Server: A Comprehensive Guide

Alright ! Let’s talk about web servers in Python using the http.server module but before we dive too deep, let’s be clear that this isn’t a recommended solution for production environments due to security and performance limitations. The official documentation warns us right off the bat: “Warning: http.server is not recommended for production.”

That being said, if you’re just starting out with web development or need a quick and dirty way to serve up some static files, this module can be a great tool in your arsenal! Let’s take a look at how it works.

To kick things off, let’s create our basic skeleton code:

# Import the necessary modules
from http.server import BaseHTTPRequestHandler, HTTPServer # Importing the BaseHTTPRequestHandler and HTTPServer classes from the http.server module
import os # Importing the os module for accessing operating system functionalities

# Define a class for handling requests
class MyRequestHandler(BaseHTTPRequestHandler):
    """
    A class that inherits from the BaseHTTPRequestHandler class and handles incoming requests.
    """

    # Define a method for handling GET requests
    def do_GET(self):
        """
        A method that handles GET requests and sends a response back to the client.
        """
        # Set the response status code to 200 (OK)
        self.send_response(200)

        # Set the response headers
        self.send_header('Content-type', 'text/html') # Setting the content type to HTML
        self.end_headers()

        # Get the current working directory
        cwd = os.getcwd()

        # Check if the requested file exists in the current working directory
        if self.path == '/index.html' and os.path.exists(cwd + '/index.html'):
            # Open the requested file and read its contents
            with open(cwd + '/index.html', 'rb') as file:
                # Send the file contents as the response body
                self.wfile.write(file.read())
        else:
            # If the requested file does not exist, send a 404 (Not Found) response
            self.send_error(404, 'File not found')

# Check if the script is being run directly
if __name__ == '__main__':
    # Set the server address and port
    server_address = ('localhost', 8000)

    # Create an instance of the HTTPServer class, passing in the server address and the request handler class
    httpd = HTTPServer(server_address, MyRequestHandler)

    # Print a message to indicate that the server is starting
    print('Starting server...')

    # Start the server and keep it running until it is manually stopped
    httpd.serve_forever()

This code sets up a basic framework for our custom web server using the BaseHTTPRequestHandler class from the http.server module. We’re also importing os to handle file system operations later on.

The MyRequestHandler class is where we’ll define all of our fancy request handling logic, but let’s hold off on that for a bit. For now, let’s focus on starting up the server and getting it running!

When you run this code (assuming your Python interpreter is in your system path), you should see something like:

# Importing necessary modules
import socket # Importing the socket module to establish network connections
import threading # Importing the threading module to handle multiple requests simultaneously

# Defining a class for handling requests
class RequestHandler:
    # Initializing the class with a socket object and a client address
    def __init__(self, client_socket, client_address):
        self.client_socket = client_socket # Assigning the client socket to an instance variable
        self.client_address = client_address # Assigning the client address to an instance variable

    # Defining a method to handle incoming requests
    def handle_request(self):
        # Receiving data from the client
        data = self.client_socket.recv(1024) # Receiving up to 1024 bytes of data from the client
        # Checking if data is received
        if data:
            # Printing the received data
            print("Received data from {}: {}".format(self.client_address, data)) # Printing the client address and the received data
            # Sending a response back to the client
            self.client_socket.send(b"Server received your request") # Sending a response back to the client in bytes format
        # Closing the client socket
        self.client_socket.close() # Closing the client socket after handling the request

# Defining a function to start the server
def start_server():
    # Creating a socket object
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Creating a TCP socket object
    # Binding the socket to a host and port
    server_socket.bind(("localhost", 8000)) # Binding the socket to the localhost and port 8000
    # Listening for incoming connections
    server_socket.listen(5) # Allowing up to 5 connections to be queued
    print("Server is now listening on port 8000...") # Printing a message to indicate the server is now listening

    # Handling incoming requests
    while True:
        # Accepting a connection
        client_socket, client_address = server_socket.accept() # Accepting a connection and assigning the client socket and address to variables
        # Creating a thread to handle the request
        request_handler = RequestHandler(client_socket, client_address) # Creating an instance of the RequestHandler class
        request_thread = threading.Thread(target=request_handler.handle_request) # Creating a thread to handle the request
        request_thread.start() # Starting the thread to handle the request

# Starting the server
print("Starting server...") # Printing a message to indicate the server is starting
start_server() # Calling the start_server function to start the server

Congratulations, you just started a web server using Python! But let’s make sure we can actually access it. Open up your favorite browser and navigate to http://localhost:8000 (assuming that’s the IP address and port number you used in the code). You should see something like this:

# This script is used to handle a 404 error when a requested resource is not found on the server.

# Import the necessary modules for creating a web server
import http.server
import socketserver

# Define the port number to be used for the server
PORT = 8000

# Create a class that inherits from the BaseHTTPRequestHandler class to handle HTTP requests
class Handler(http.server.BaseHTTPRequestHandler):
    
    # Override the do_GET method to handle GET requests
    def do_GET(self):
        
        # Set the response status code to 404
        self.send_response(404)
        
        # Set the content type of the response to text/plain
        self.send_header('Content-type', 'text/plain')
        
        # End the headers
        self.end_headers()
        
        # Write the error message to the response body
        self.wfile.write(b'The requested resource / was not found on this server.')
        
# Create a socket server using the Handler class and specify the port number
with socketserver.TCPServer(("", PORT), Handler) as httpd:
    
    # Print a message to indicate that the server is running
    print("Server running on port", PORT)
    
    # Start the server and keep it running until interrupted
    httpd.serve_forever()
    
# To access the server, open a web browser and navigate to http://localhost:8000

That’s because we haven’t defined any handlers for our requests yet! Let’s fix that by adding some logic to the MyRequestHandler class.

First, let’s add a method called do_GET() which will handle HTTP GET requests:

# Define a class called MyRequestHandler that inherits from the BaseHTTPRequestHandler class
class MyRequestHandler(BaseHTTPRequestHandler):
    # Define a method called do_GET that takes in the self parameter
    def do_GET(self):
        # Handle GET requests here!
        # This method will handle HTTP GET requests and is currently empty. 
        # It needs to be filled with logic to handle the requests. 
        # The self parameter refers to the current instance of the class, allowing us to access its attributes and methods. 
        # The do_GET() method is a built-in method in the BaseHTTPRequestHandler class that we are inheriting from. 
        # It is called whenever a GET request is received by the server. 
        # By defining our own do_GET() method, we can customize how the server handles these requests. 
        # We will add the necessary logic to handle GET requests in the next steps. 
        
    # Add ellipsis to indicate that there are more methods and code to be added to the class.

Inside the do_GET() method, we’ll check if the requested resource is a file that exists in our local directory. If it does, we’ll send back the contents of that file as the response body:

class MyRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            # Remove leading '/' from URL path
            path = self.path[1:] 
            # Check if requested resource exists in local directory
            if os.path.exists(os.getcwd() + '/' + path):
                # Open file in read binary mode
                with open(os.getcwd() + '/' + path, 'rb') as f:
                    # Read contents of file
                    contents = f.read()
                    # Set response status code to OK (200)
                    self.send_response(200)
                    # Set content type header to application/octet-stream
                    self.send_header('Content-type', 'application/octet-stream')
                    # Send headers and start body
                    self.end_headers()
                    # Write contents of file to response body
                    self.wfile.write(contents)
            else:
                # Send error status code (404) with message in response body
                self.send_error(404, 'File not found!')
        except Exception as e:
            # Log any errors to console for debugging purposes
            print('Error handling GET request:', str(e))
            
    ...

We’re also adding a do_POST() method which will handle HTTP POST requests. This is where you can get creative and implement some fancy logic based on the data being sent in the request body:

# Adding annotations to the python script

# Defining a class for handling HTTP requests
class MyRequestHandler(BaseHTTPRequestHandler):
    
    # Defining a method for handling HTTP GET requests
    def do_GET(self):
        # Code for handling GET requests goes here
        ...

    # Defining a method for handling HTTP POST requests
    def do_POST(self):
        try:
            # Getting the length of the request body from the headers
            content_length = int(self.headers['Content-Length'])
            # Reading and decoding the request body data
            body = self.rfile.read(content_length).decode()
            
            # Do something with the POST data here!
            # This is where you can implement your own logic based on the data sent in the request body
            
        except Exception as e:
            # Logging any errors to the console for debugging purposes
            print('Error handling POST request:', str(e))
        
    # Other methods for handling different types of HTTP requests can be added here
    ...

And that’s it! You now have a basic custom web server using Python and the http.server module, but remember this isn’t recommended for production environments due to security and performance limitations. For more advanced use cases or larger-scale projects, you may want to consider other tools like Flask or Django instead.

SICORPS