Build Your Own Python HTTP File Server From scratch!

Sidharth SunuSidharth Sunu
7 min read

Hi everyone! This is my first blog, and I thought, why not kick it off with one of my recent projects: an HTTP file server.

I worked on this project last weekend. I had two choices: C or Python. While C was fine, I decided Python was better for learning the core concepts. If I wanted to dive deeper, I could always redo it in C later.

So, what is an HTTP server, and why build one? Simply put, an HTTP server accepts requests from a browser and sends back responses, like HTML files. Now, why bother building one? Personally, I've never been a huge fan of frontend work, but I love understanding backend details, like setting up servers with Proxmox or how services like Nginx or Apache function. So, I thought this would be a great starting point.

Enough talking, you might say. How do you build one?
Let's start with the code. I'll share the complete code first (it's not very long), and then we'll explain it line by line.
We'll begin with a basic HTTP server, without file serving.

from http.server import HTTPServer, BaseHTTPRequestHandler
import time

HOST = "192.168.0.124"
PORT = 9999

class SidhHTTP(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()

        self.wfile.write(bytes("<html><body><h1>Hello World</h1></body></html>", "utf-8"))

    def do_POST(self):
        self.send_response(200)
        self.send_header("Content-type", "application/json")
        self.end_headers()
        date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        self.wfile.write(bytes(f'{{"time": "{date}"}}', "utf-8"))

server = HTTPServer((HOST, PORT), SidhHTTP)
print("server running...")
server.serve_forever()
server.server_close()
print("server closed...")
  • Importing Modules
    We import HTTPServer and BaseHTTPRequestHandler from Python's built-in http.server module. These provide the basic building blocks for creating an HTTP server. We also import the time module to demonstrate dynamic content in a POST request.

  • Defining Host and Port
    The HOST variable defines the IP address where the server will run. This can be your local machine’s IP address or 127.0.0.1 for localhost.
    The PORT is any available port number (above 1024 if you're not running as root).

  • Creating a Request Handler Class
    We define a class SidhHTTP that inherits from BaseHTTPRequestHandler. This class will handle incoming HTTP requests.

  • Handling GET Requests: do_GET
    This method gets triggered whenever a client (like a browser or Postman) makes a GET request.

    • send_response(200) sends an HTTP status code 200 (OK) to indicate a successful response.

    • send_header() sets the HTTP response headers. In this case, we declare the content type as text/html.

    • end_headers() finalizes the header section. From this point, we can start sending the actual response body.

    • self.wfile.write() writes the response body. Since this method expects bytes, we encode the string using UTF-8.

  • Handling POST Requests: do_POST
    Similar to do_GET, but here we return a JSON object with the current time.

    • We format the current date/time using time.strftime().

    • Then we send a JSON response like {"time": "2025-06-24 11:52:34"} by writing it as bytes.

  • Starting the Server

    • HTTPServer((HOST, PORT), SidhHTTP) creates an HTTP server instance that listens on the given IP and port, and uses SidhHTTP as the request handler class.

    • serve_forever() starts an infinite loop to keep handling incoming requests until interrupted.

    • server_close() is called after the loop is broken (e.g., when the program is stopped), to clean up resources.

To start the server, open the terminal and run python main.py (or whatever your file is named). This will give you an IP address that you can enter into your browser, and voilà! You've created your own Python HTTP server. Let's go!

That was the basic version, which I recommend trying out on your own first to get a feel for it.

Now, let's move on to a mini file server that shows the files in the /files directory of this server. I'll provide the code and start explaining:

from http.server import HTTPServer, BaseHTTPRequestHandler
import time
import os
import mimetypes

HOST = "192.168.0.124"
PORT = 9999
FILE_DIR = os.path.join(os.path.dirname(__file__), "files")

class SidhHTTP(BaseHTTPRequestHandler):

    def do_GET(self):
        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-type","text/html")
            self.end_headers()

            self.wfile.write(bytes("<html><body><h1>Hello World</h1></body></html>","utf-8"))
        elif(self.path == "/files/"):
            try:
                print("Looking in folder:", os.path.abspath(FILE_DIR))
                files = os.listdir(FILE_DIR)
                print(files)
                self.send_response(200)
                self.send_header("Content-type","text/html")
                self.end_headers()

                html = "<h1>Files in server</h1><ul>"
                for f in files:
                    file_url = f"/files/{f}"
                    html += f"<li><a href='{file_url}'>{f}</a></li>"
                html += "</ul>"
                self.wfile.write(html.encode())
            except FileNotFoundError:
                self.send_error(500,"Files directory not found")
        elif(self.path.startswith("/files/")):
            filename = self.path[len("/files/"):]
            filepath = os.path.join(FILE_DIR, filename)

            if os.path.isfile(filepath):
                mime_type, _ = mimetypes.guess_type(filepath)
                mime_type = mime_type or "application/octet-stream"

                self.send_response(200)
                self.send_header("Content-type",mime_type)
                self.send_header("Content-Disposition",f'attachment; filename="{filename}"')
                self.end_headers()

                with open(filepath,"rb") as f:
                    self.wfile.write(f.read())
            else:
                self.send_error(404,"File Not Found")
        else:
            self.send_error(404,"Page not Found")

    def do_POST(self):
        self.send_response(200)
        self.send_header("Content-type","application/json")
        self.end_headers()
        date = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(time.time()))
        self.wfile.write(bytes('{"time": "'+ date + '"}',"utf-8"))

server = HTTPServer((HOST,PORT), SidhHTTP)
print("server running...")
server.serve_forever()
server.server_close()

print("server closed...")

Lets Understand this Line by Line, mainly the part where it focuses on the file, as that is essentially the major new diff.

        elif(self.path == "/files/"):
            try:
                print("Looking in folder:", os.path.abspath(FILE_DIR))
                files = os.listdir(FILE_DIR)
                print(files)
                self.send_response(200)
                self.send_header("Content-type","text/html")
                self.end_headers()

                html = "<h1>Files in server</h1><ul>"
                for f in files:
                    file_url = f"/files/{f}"
                    html += f"<li><a href='{file_url}'>{f}</a></li>"
                html += "</ul>"
                self.wfile.write(html.encode())
            except FileNotFoundError:
                self.send_error(500,"Files directory not found")
  • Check for /files/ URL
    This block activates when the client accesses /files/, meaning they want to view all available files.

  • List all files in the files/ directory
    We use os.listdir(FILE_DIR) to retrieve a list of all files inside the directory specified by FILE_DIR. This is wrapped in a try-except block to catch the case where the directory doesn't exist.

  • Send the response header
    We send a status code 200 OK, then a header specifying that the content is text/html, and finally call end_headers() to indicate that we’re done with headers.

  • Generate the HTML page dynamically
    Using a for loop, we build a simple HTML unordered list (<ul>) with one link (<a>) per file. The href for each link is /files/<filename>, which we’ll handle in the next section.

  • Write the HTML back to the client
    The generated HTML is encoded to bytes and written to the response.

  • Handle errors gracefully
    If the folder doesn't exist, we catch the FileNotFoundError and return a 500 Internal Server Error.

Then we Move on the the next section, Which is making the actual individual file links downloadable:

elif(self.path.startswith("/files/")):
            filename = self.path[len("/files/"):]
            filepath = os.path.join(FILE_DIR, filename)

            if os.path.isfile(filepath):
                mime_type, _ = mimetypes.guess_type(filepath)
                mime_type = mime_type or "application/octet-stream"

                self.send_response(200)
                self.send_header("Content-type",mime_type)
                self.send_header("Content-Disposition",f'attachment; filename="{filename}"')
                self.end_headers()

                with open(filepath,"rb") as f:
                    self.wfile.write(f.read())
            else:
  • Check if the path starts with /files/
    This means the user is trying to access a specific file. Example: /files/my_photo.jpg.

  • Extract the filename
    We slice the path to remove /files/ and get the filename using:
    filename = self.path[len("/files/"):]

  • Build the full file path
    We use os.path.join(FILE_DIR, filename) to build the absolute path to the file.

  • Check if the file exists
    os.path.isfile(filepath) ensures that what we’re pointing to is actually a file.

  • Determine the MIME type

      • MIME stands for Multipurpose Internet Mail Extensions.

        • Originally designed for email attachments, it's now heavily used in HTTP to describe the type of content being transferred.

        • Examples:

          • .htmltext/html

          • .pngimage/png

          • .mp3audio/mpeg

        • Python’s mimetypes.guess_type() uses the file extension to return the MIME type. It uses an internal mapping/dictionary for known types.

  • mimetypes.guess_type(filepath) tries to determine the file type based on its extension.

  • If it fails (i.e., returns None), we fall back to "application/octet-stream" — a generic type for binary data.

  • Send download headers

    • Content-type: Informs the browser what kind of file is being served.

    • Content-Disposition: attachment; filename="xyz": Tells the browser to treat it as a downloadable file instead of displaying it inline (especially for things like PDFs or images).

  • Read the file in binary mode and write it to the response
    We open the file using with open(filepath, "rb"), read its bytes, and send them to the client via self.wfile.write().

  • If the file doesn’t exist
    We send a 404 File Not Found error.

To run this, simply execute main.py again, and you'll get an IP address to open in your web browser. This will show the same old hello page as before. However, this time, add /files/ to the URL, and it will display all the files in the /files directory you created (make sure it's in the same directory as the main.py file). This can be very practical. Imagine you want to share files between two laptops or desktops. Instead of dealing with FTP or SMB, you can easily run this on one device and have the other device connect to that IP to download the files. It's that simple!

If you've used Django, Flask, or similar frameworks, you might notice we're moving towards something similar—a Python web framework. I'll write a blog about that too, so stay tuned. Why not make that our next step? See you all there!

1
Subscribe to my newsletter

Read articles from Sidharth Sunu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sidharth Sunu
Sidharth Sunu