Build Your Own Python HTTP File Server From scratch!

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 importHTTPServer
andBaseHTTPRequestHandler
from Python's built-inhttp.server
module. These provide the basic building blocks for creating an HTTP server. We also import thetime
module to demonstrate dynamic content in a POST request.Defining Host and Port
TheHOST
variable defines the IP address where the server will run. This can be your local machine’s IP address or127.0.0.1
for localhost.
ThePORT
is any available port number (above 1024 if you're not running as root).Creating a Request Handler Class
We define a classSidhHTTP
that inherits fromBaseHTTPRequestHandler
. 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 astext/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 todo_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 usesSidhHTTP
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 useos.listdir(FILE_DIR)
to retrieve a list of all files inside the directory specified byFILE_DIR
. This is wrapped in atry-except
block to catch the case where the directory doesn't exist.Send the response header
We send a status code200 OK
, then a header specifying that the content istext/html
, and finally callend_headers()
to indicate that we’re done with headers.Generate the HTML page dynamically
Using afor
loop, we build a simple HTML unordered list (<ul>
) with one link (<a>
) per file. Thehref
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 theFileNotFoundError
and return a500 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 useos.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:
.html
→text/html
.png
→image/png
.mp3
→audio/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 usingwith open(filepath, "rb")
, read its bytes, and send them to the client viaself.wfile.write()
.If the file doesn’t exist
We send a404 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!
Subscribe to my newsletter
Read articles from Sidharth Sunu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
