🖥️ HTTP Server¶
Building web servers with Python's built-in http.server module
What is http.server?¶
The http.server module is Python's built-in library for creating HTTP servers. It provides:
HTTPServerclass - The server that listens for connectionsBaseHTTPRequestHandlerclass - Handles incoming HTTP requests- Zero dependencies - Part of Python's standard library
Think of it like a small restaurant that: - Opens its doors (binds to a port) - Waits for customers (listens for requests) - Takes orders (reads HTTP requests) - Serves food (sends HTTP responses)
Basic Server Structure¶
from http.server import HTTPServer, BaseHTTPRequestHandler
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Handle GET requests
self.send_response(200) # OK status
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b'Hello, World!')
# Create and start server
server = HTTPServer(('localhost', 8000), MyHandler)
print('Server running on http://localhost:8000')
server.serve_forever()
Key Components:
| Component | Purpose |
|---|---|
HTTPServer |
The actual server that listens for connections |
BaseHTTPRequestHandler |
Base class for handling requests |
do_GET() |
Method called for GET requests |
send_response(code) |
Sets HTTP status code |
send_header(name, value) |
Adds HTTP headers |
end_headers() |
Finishes header section |
wfile.write(data) |
Sends response body |
Understanding Request Handlers¶
The Handler Class¶
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Called when browser requests a page
pass
def do_POST(self):
# Called when form is submitted
pass
def log_message(self, format, *args):
# Override to customize logging
print(f"[{self.date_time_string()}] {args[0]}")
Request Information Available¶
def do_GET(self):
# The requested path (e.g., "/about")
path = self.path
# HTTP method used (GET, POST, etc.)
method = self.command
# Request headers (dictionary-like)
user_agent = self.headers.get('User-Agent')
# Client address
client_ip = self.client_address[0]
HTTP Status Codes¶
Always send appropriate status codes:
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Request succeeded |
| 301 | Moved Permanently | Resource moved to new URL |
| 302 | Found | Temporary redirect |
| 400 | Bad Request | Request was malformed |
| 404 | Not Found | Resource doesn't exist |
| 405 | Method Not Allowed | Wrong HTTP method |
| 500 | Internal Server Error | Server error occurred |
def do_GET(self):
if self.path == '/':
self.send_response(200)
elif self.path == '/old-page':
self.send_response(301)
self.send_header('Location', '/new-page')
else:
self.send_response(404)
Serving Different Content Types¶
def do_GET(self):
self.send_response(200)
# Set content type based on what's being served
if self.path.endswith('.html'):
self.send_header('Content-type', 'text/html')
elif self.path.endswith('.css'):
self.send_header('Content-type', 'text/css')
elif self.path.endswith('.js'):
self.send_header('Content-type', 'application/javascript')
elif self.path.endswith('.json'):
self.send_header('Content-type', 'application/json')
elif self.path.endswith('.png'):
self.send_header('Content-type', 'image/png')
else:
self.send_header('Content-type', 'text/plain')
self.end_headers()
Serving Static Files¶
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
class StaticFileHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Map URL path to file path
if self.path == '/':
filepath = 'index.html'
else:
# Remove leading slash
filepath = self.path.lstrip('/')
# Security: prevent directory traversal
filepath = os.path.normpath(filepath)
if filepath.startswith('..'):
self.send_error(403, "Access denied")
return
# Check if file exists
if not os.path.exists(filepath):
self.send_error(404, "File not found")
return
# Read and serve file
try:
with open(filepath, 'rb') as f:
content = f.read()
self.send_response(200)
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
except IOError:
self.send_error(500, "Server error")
Complete Example: Simple Web Server¶
"""
simple_server.py - A basic HTTP server with multiple routes
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class SimpleHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Route based on path
if self.path == '/':
self.serve_home()
elif self.path == '/about':
self.serve_about()
elif self.path == '/api/data':
self.serve_api_data()
else:
self.serve_404()
def serve_home(self):
html = """<!DOCTYPE html>
<html>
<head><title>Home</title></head>
<body>
<h1>Welcome!</h1>
<nav>
<a href="/">Home</a> |
<a href="/about">About</a> |
<a href="/api/data">API Data</a>
</nav>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_about(self):
html = """<!DOCTYPE html>
<html>
<head><title>About</title></head>
<body>
<h1>About This Server</h1>
<p>Built with Python's http.server module!</p>
<a href="/">← Back Home</a>
</body>
</html>"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
def serve_api_data(self):
data = {'message': 'Hello from API', 'status': 'ok'}
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def serve_404(self):
self.send_response(404)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b'<h1>404 - Page Not Found</h1>')
def log_message(self, format, *args):
# Custom logging
print(f"[{self.date_time_string()}] {self.client_address[0]} - {args[0]}")
# Start server
if __name__ == '__main__':
server = HTTPServer(('localhost', 8000), SimpleHandler)
print('Server running at http://localhost:8000')
print('Press Ctrl+C to stop')
try:
server.serve_forever()
except KeyboardInterrupt:
print('\nShutting down...')
server.shutdown()
Handling POST Requests¶
def do_POST(self):
# Get content length from headers
content_length = int(self.headers.get('Content-Length', 0))
# Read the request body
post_data = self.rfile.read(content_length)
# Parse form data (application/x-www-form-urlencoded)
from urllib.parse import parse_qs
form_data = parse_qs(post_data.decode())
# Handle the data
username = form_data.get('username', [''])[0]
# Send response
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(f'Hello, {username}!'.encode())
Common Mistakes¶
| Mistake | Why It's Wrong | Correct Approach |
|---|---|---|
Not calling end_headers() |
Browser won't receive response | Always call after sending headers |
Writing strings to wfile |
wfile expects bytes |
Use .encode() or b'...' |
| Not checking file paths | Security vulnerability | Use os.path.normpath() and validation |
Blocking serve_forever() |
Can't stop server cleanly | Use threads or signal handlers |
| Forgetting content length | May cause connection issues | Set Content-Length header |
Quick Reference¶
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class QuickHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b'<h1>Hello!</h1>')
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
data = self.rfile.read(length)
# Process data...
self.send_response(200)
self.end_headers()
# Run server
server = HTTPServer(('localhost', 8000), QuickHandler)
server.serve_forever()
Start server in one line (for development only):
Next Steps¶
Now that you can build a basic server, let's learn how to route different URLs!
→ Continue to 03: Simple Routing