Skip to content

Instantly share code, notes, and snippets.

@notdaniel
Last active March 9, 2025 07:15
Show Gist options
  • Save notdaniel/de88c1e7a548c0f0820f2c53b3af5901 to your computer and use it in GitHub Desktop.
Save notdaniel/de88c1e7a548c0f0820f2c53b3af5901 to your computer and use it in GitHub Desktop.
Slightly improved version of Python's simple HTTP server enabling threading, CORS, and custom headers.
#!/usr/bin/env python3
"""
Slightly improved version of the simple HTTP server from the Python stdlib.
Provides options for a threaded server, CORS headers, and custom headers.
"""
from argparse import ArgumentParser, Namespace
from http.server import HTTPServer, SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
def valid_header(header: str) -> tuple[str, str]:
"""Validate a header string."""
try:
name, value = header.split(":", 1)
return name.strip(), value.strip()
except ValueError as e:
raise ValueError(f"Invalid header format: {header}") from e
def valid_path(path: str) -> str:
"""Validate a path string."""
p = Path(path)
if not p.exists() or not p.is_dir():
raise ValueError(f"Invalid path: {path}")
return str(p.resolve())
class CustomHeadersHTTPRequestHandler(SimpleHTTPRequestHandler):
"""
Extends SimpleHTTPRequestHandler to add custom response headers
while keeping all other default behavior.
"""
# Default custom headers (can be extended via command line)
CUSTOM_HEADERS: dict[str, str] = {}
DIRECTORY: str = "."
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, directory=self.DIRECTORY, **kwargs)
def end_headers(self) -> None:
"""
Override end_headers method to add custom headers before
the original method sends the blank line signaling end of headers.
"""
# Add all custom headers
for name, value in self.CUSTOM_HEADERS.items():
self.send_header(name, value)
# Call the original end_headers method from the parent class
super().end_headers()
def run_server(
port: int = 8000,
bind: str = "0.0.0.0",
directory: str = ".",
threading: bool = False,
enable_cors: bool = False,
custom_headers: list[tuple[str, str]] | None = None,
) -> None:
"""
Run an HTTP server with the specified configuration.
Args:
port: Port number to listen on
bind: Address to bind to (empty string means all interfaces)
threading: Whether to use a threaded server
enable_cors: Whether to enable CORS headers
custom_headers: Additional headers to add to all responses
"""
server_address = (bind, port)
# Start with default headers from CustomHeadersHTTPRequestHandler
handler_headers = CustomHeadersHTTPRequestHandler.CUSTOM_HEADERS.copy()
# Add CORS headers if enabled
if enable_cors:
handler_headers.update(
{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
}
)
# Add custom headers from command line
if custom_headers:
for key, value in custom_headers:
handler_headers[key] = value
# Create a configured handler class with our custom headers
class ConfiguredHandler(CustomHeadersHTTPRequestHandler):
CUSTOM_HEADERS: dict[str, str] = handler_headers
DIRECTORY: str = directory
# Choose between regular HTTPServer and ThreadingHTTPServer
server_class: type[HTTPServer] = ThreadingHTTPServer if threading else HTTPServer
with server_class(server_address, ConfiguredHandler) as httpd:
host_name: str = bind
print(f"Server started at http://{host_name}:{port}")
print(f"Serving from path: {directory}")
print(f"Using server type: {server_class.__name__}")
if ConfiguredHandler.CUSTOM_HEADERS:
print("Custom headers:")
for name, value in ConfiguredHandler.CUSTOM_HEADERS.items():
print(f" {name}: {value}")
print("Press Ctrl+C to stop")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped.")
def main() -> None:
parser: ArgumentParser = ArgumentParser(
description="HTTP Server with threading and custom response headers"
)
parser.add_argument(
"--port", "-p", type=int, default=8000, help="Port to listen on (default: 8000)"
)
parser.add_argument(
"--bind",
"-b",
default="0.0.0.0",
help="Address to bind to (default: all interfaces)",
)
parser.add_argument(
"--dir",
"-d",
dest="directory",
type=valid_path,
default=".",
help="Directory to serve (default: current directory)",
)
parser.add_argument(
"--cors",
"-c",
action="store_true",
help="Enable CORS headers",
)
parser.add_argument(
"--no-threading",
"-n",
action="store_true",
help="Use non-threaded server",
)
parser.add_argument(
"--header",
"-H",
type=valid_header,
action="append",
dest="headers",
metavar="KEY:VAL",
help="Add custom header (repeatable)",
)
args: Namespace = parser.parse_args()
run_server(
port=args.port,
bind=args.bind,
directory=args.directory,
threading=not args.no_threading,
enable_cors=args.cors,
custom_headers=args.headers,
)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment