Build a Python-Based Simple Web Server with Custom Request Handling
A small web server is an excellent way to understand how HTTP moves between a browser and an application. Python includes batteries-included modules for opening a listening socket, parsing requests, returning files and generating responses, so a useful prototype can be built without installing a full framework.
This kind of project suits developers who want a compact Linux utility for testing APIs, serving a local dashboard or learning how routing works beneath Flask and Django. It can run on a laptop in Melbourne, a Raspberry Pi on a home network in Perth or a development VM hosted in Sydney.
The example below uses Python’s standard library and deliberately keeps the design visible. It is appropriate for local development and controlled networks, but it should not be treated as a production internet-facing service without authentication, hardened configuration and proper operational controls.
Choose the Server’s Scope and Runtime
Start by defining what the program must do. A basic implementation can respond to GET requests, return a JSON status document, serve a small set of static files and reject unsupported methods. This narrow scope makes request handling easier to inspect and test.
Python 3.10 or later is a sensible baseline because it provides modern type hints and reliable standard-library behaviour. Create a project directory, save the server as server.py, and use a virtual environment if you later add dependencies. For a standard-library-only tool, running it with python3 server.py is enough.
The listening address matters. Binding to 127.0.0.1 keeps the service on the local machine, while 0.0.0.0 exposes it to other devices on the network. In an Australian home using an NBN router, local access may be useful for testing a phone or tablet, but public exposure should never happen by accident.
Create a Minimal HTTP Server
Python’s http.server module provides HTTPServer and BaseHTTPRequestHandler. The server accepts TCP connections, parses the HTTP request line and headers, then calls a method such as do_GET on a handler object.
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from urllib.parse import urlparse
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
path = urlparse(self.path).path
if path == "/":
body = b"<h1>Python server is running</h1>"
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if path == "/api/status":
payload = json.dumps({"status": "ok"}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
return
self.send_error(404, "Resource not found")
server = ThreadingHTTPServer(("127.0.0.1", 8000), RequestHandler)
print("Listening on http://127.0.0.1:8000")
server.serve_forever()
The response must include a status code, headers and a body in that order. Content-Length tells the browser how many bytes to read, while the content type describes the payload. Encoding the body explicitly avoids confusion when non-ASCII characters appear in Australian addresses, names or documentation.
Run the program and visit http://127.0.0.1:8000/api/status. The handler receives the complete request path, so parsing it before routing prevents query strings from interfering with path comparisons.
Add Clear Custom Request Handling
Custom handling becomes more maintainable when routing, validation and response generation are separate operations. A route such as /api/status?region=nsw can be parsed with urlparse, while parse_qs can validate query parameters. Never assume that a browser or script sends well-formed input.
A useful handler should also implement do_POST when the application accepts data. Read the declared body length from the Content-Length header, limit its size, decode it safely and return 400 Bad Request for malformed JSON. Returning meaningful HTTP status codes makes command-line testing and monitoring much easier.
Requests Worth Testing
GET /returns HTML with status200GET /api/statusreturns valid JSON- An unknown path returns
404 - Unsupported methods return
405
Override log_message if the default access log is too noisy, but keep enough information to trace failures. A timestamp, method, path and response status are usually more valuable than dumping every header, particularly when logs contain personal or session-related data.
Serve Files Without Creating a Security Hole
Static file support is convenient for CSS, JavaScript and images, yet directly joining a URL path to a filesystem path can enable directory traversal. A request containing ../ must never escape the chosen document root. Resolve both paths and verify that the requested file remains inside the approved directory.
Use pathlib.Path for readable path operations and reject directories, symbolic-link surprises and files that are too large for the service’s purpose. A simple development server can support only known extensions, which reduces accidental exposure of configuration files such as .env, private keys or database backups.
from pathlib import Path
WEB_ROOT = Path("public").resolve()
def safe_file_path(url_path):
relative = Path(url_path.lstrip("/"))
candidate = (WEB_ROOT / relative).resolve()
if WEB_ROOT not in candidate.parents:
return None
if not candidate.is_file():
return None
return candidate
MIME types should be selected with mimetypes.guess_type, while missing files should receive a consistent 404 response. Do not place the server’s Python source, SSH keys or user-upload directories beneath public.
Handle Multiple Clients and Record Activity
ThreadingHTTPServer gives each request its own thread, allowing a browser to load several assets without making every connection wait for the previous one. This is suitable for demonstrations and small internal tools, although it does not replace a production application server. Threads can still consume memory when clients open many connections.
Keep request work brief. Avoid long-running calculations, unbounded uploads and calls to unreliable external services inside the handler. If a task takes time, place it in a queue or use an architecture designed for asynchronous work.
Useful Operational Signals
- Request count grouped by status code
- Response duration in milliseconds
- Rejected methods and oversized bodies
- Exceptions with a generated request identifier
A small access log can reveal a broken frontend quickly. For example, a developer in Brisbane might discover that a browser is requesting /static/app.js while the server only exposes /assets/app.js. Log timestamps consistently in UTC even when the team works across Sydney, Adelaide and Perth time zones.
Test and Run the Service Responsibly
Test the server with a browser, curl and Python’s unittest. curl -i http://127.0.0.1:8000/api/status shows headers as well as the response body, making it useful for checking content types and status codes. Include malformed paths, missing headers, large payloads and unexpected methods in the test cases.
For local network testing, bind to the machine’s LAN address only when necessary and configure the firewall to allow the chosen port. Australian internet providers and routers commonly use network address translation, but that is not a security guarantee. Avoid port forwarding a learning project to the public internet.
If the server handles names, email addresses, account details or other personal information, consider obligations under Australia’s Privacy Act 1988 and the Australian Privacy Principles. Keep development data fictional, minimise logs and protect stored information. A production service should normally sit behind a reverse proxy with TLS, authentication, rate limiting and a maintained application stack.
This compact Python web server demonstrates the core relationship between sockets, HTTP parsing, routing and response generation. It is small enough to understand line by line, while still providing a practical foundation for Linux utilities, internal dashboards and open-source experiments.
