Static HTTP File Server (python, written by Codex)
envgap__codex__python-t1-28
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
NameError: name log_fp is not defined - class variable referencing outer scope
Not a benchmark task.
- Its repair changed source code, so it is not an environment task.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
requirements.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/python-t1 #28 · read the task the agent was given
Codex wrote this python project from the task below. It does not run on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Static HTTP File Server Write a program that serves static files over HTTP from a local directory, supporting directory listings, MIME type detection, caching headers, range requests for large files, and access logging. FUNCTIONAL REQUIREMENTS: - Accept a directory path to serve as a command-line argument (default: current directory) - Start an HTTP server on a configurable port via --port flag (default: 8080) and bind address via --host flag (default: 0.0.0.0) - Serve files with correct MIME types auto-detected from file extensions (HTML, CSS, JS, images, fonts, JSON, XML, PDF, video, audio, etc.) - Generate directory listing pages when accessing a directory URL, showing file names, sizes, modification dates, and file type icons, with a --no-listing flag to disable this - Support index files: automatically serve index.html or index.htm if present in a directory - Implement HTTP caching headers: ETag based on file content hash, Last-Modified from file timestamp, and Cache-Control with configurable max-age via --cache flag (default: 3600 seconds) - Support HTTP Range requests for partial content delivery (needed for video/audio streaming and resumable downloads) - Log all requests to console in Common Log Format (CLF): remote address, timestamp, method, path, status code, response size, and response time - Save access logs to a file via --log flag - Support CORS headers via --cors flag to enable cross-origin requests (configurable allowed origins) - Support HTTPS via --ssl flag with --cert and --key flags for certificate and private key file paths - Print server startup information: URL, served directory, features enabled - If no directory is given, create a sample directory with HTML, CSS, JS, image, and text files, then start serving them and print the URL to access each - Handle errors: port already in use, permission denied, symlink traversal prevention, and graceful shutdown on SIGINT Create a complete Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include: - Source code - requirements.txt with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
README.md
# Static HTTP File Server (Python) Serves static files over HTTP/HTTPS with MIME detection, directory listing, cache headers, range requests, CORS, and CLF access logging. ## Requirements - Ubuntu 22.04 - Python 3.10+ ## Dependencies - No external dependencies (Python standard library only) - `requirements.txt` is intentionally empty of third-party packages ## Setup ```bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` ## Run Serve a directory: ```bash python3 src/main.py /path/to/serve ``` Custom host/port/cache: ```bash python3 src/main.py . --host 127.0.0.1 --port 9090 --cache 120 ``` Disable listing and enable CORS: ```bash python3 src/main.py . --no-listing --cors "*" ``` Access log file: ```bash python3 src/main.py . --log access.log ``` HTTPS: ```bash python3 src/main.py . --ssl --cert cert.pem --key key.pem ``` Demo mode (no directory argument): ```bash python3 src/main.py ``` ## Features - Correct MIME type serving - Directory listings with name/size/modified/type indicator - `index.html` / `index.htm` support - `ETag`, `Last-Modified`, and `Cache-Control` - HTTP range (`Range: bytes=...`) support - CLF-style logging to console and optional file - CORS via `--cors` - HTTPS via `--ssl --cert --key` - Symlink traversal prevention - Graceful `SIGINT` shutdown ## Example startup output ```text Static HTTP File Server started URL: http://0.0.0.0:8080 Serving directory: /home/user/project Directory listing: enabled Caching max-age: 3600 CORS: disabled TLS: disabled ```
requirements.txt
# No external dependencies required. # This project uses only Python 3.10+ standard library modules.
src/main.py
#!/usr/bin/env python3
"""Static HTTP file server with directory listing, caching, ranges, logging, CORS, and HTTPS."""
from __future__ import annotations
import argparse
import datetime as dt
import email.utils
import hashlib
import html
import mimetypes
import os
import signal
import ssl
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Optional, TextIO
from urllib.parse import unquote, urlsplit
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Static HTTP File Server")
parser.add_argument("directory", nargs="?", help="Directory to serve")
parser.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
parser.add_argument("--port", type=int, default=8080, help="Port (default: 8080)")
parser.add_argument("--no-listing", action="store_true", help="Disable directory listing")
parser.add_argument("--cache", type=int, default=3600, help="Cache-Control max-age seconds")
parser.add_argument("--log", dest="log_path", help="Access log file path")
parser.add_argument("--cors", help="Allowed origin for CORS, e.g. '*' or https://site.example")
parser.add_argument("--ssl", action="store_true", help="Enable HTTPS")
parser.add_argument("--cert", help="Certificate file path for HTTPS")
parser.add_argument("--key", help="Private key file path for HTTPS")
args = parser.parse_args()
if not (1 <= args.port <= 65535):
parser.error("--port must be 1..65535")
if args.cache < 0:
parser.error("--cache must be >= 0")
if args.ssl and (not args.cert or not args.key):
parser.error("--ssl requires --cert and --key")
return args
def ensure_sample_directory(root: Path) -> None:
root.mkdir(parents=True, exist_ok=True)
(root / "assets").mkdir(exist_ok=True)
(root / "scripts").mkdir(exist_ok=True)
(root / "index.html").write_text(
"""<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Sample Static Server</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<h1>Sample Static Site</h1>
<p>Static file server is running.</p>
<img src="/sample.svg" width="180" alt="sample image" />
<p><a href="/hello.txt">Open sample text file</a></p>
<script src="/scripts/app.js"></script>
</body>
</html>
""",
encoding="utf-8",
)
(root / "assets" / "style.css").write_text(
"body { font-family: sans-serif; margin: 2rem; } h1 { color: #1a5d8f; }\n", encoding="utf-8"
)
(root / "scripts" / "app.js").write_text("console.log('Sample JavaScript loaded');\n", encoding="utf-8")
(root / "hello.txt").write_text("Hello from sample static directory.\n", encoding="utf-8")
(root / "sample.svg").write_text(
"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 120">
<rect width="320" height="120" fill="#e9f3fb"/>
<circle cx="60" cy="60" r="32" fill="#1a5d8f"/>
<text x="110" y="68" font-size="22" fill="#1a5d8f">Static Server</text>
</svg>
""",
encoding="utf-8",
)
def human_size(size: int) -> str:
units = ["B", "KB", "MB", "GB"]
value = float(size)
idx = 0
while value >= 1024 and idx < len(units) - 1:
value /= 1024.0
idx += 1
return f"{value:.0f} {units[idx]}" if idx == 0 else f"{value:.1f} {units[idx]}"
def clf_timestamp(now: dt.datetime) -> str:
return now.strftime("%d/%b/%Y:%H:%M:%S %z")
def etag_for_file(file_path: Path) -> str:
sha = hashlib.sha256()
with file_path.open("rb") as fh:
while True:
chunk = fh.read(1024 * 1024)
if not chunk:
break
sha.update(chunk)
return f'"{sha.hexdigest()}"'
def icon_for(name: str, is_dir: bool) -> str:
if is_dir:
return "[DIR]"
ext = Path(name).suffix.lower()
if ext in {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"}:
return "[IMG]"
if ext in {".mp4", ".webm", ".mp3", ".wav", ".ogg"}:
return "[MED]"
if ext in {".html", ".htm", ".css", ".js", ".json", ".xml", ".txt", ".md"}:
return "[TXT]"
return "[FIL]"
class StaticHandler(BaseHTTPRequestHandler):
server_version = "StaticHTTPFileServer/1.0"
def do_OPTIONS(self) -> None: # noqa: N802
self.send_response(204)
self._add_cors_headers()
self.end_headers()
self._write_access_log(204, 0, 0.0)
def do_GET(self) -> None: # noqa: N802
self._handle_method(send_body=True)
def do_HEAD(self) -> None: # noqa: N802
self._handle_method(send_body=False)
def log_message(self, fmt: str, *args) -> None: # noqa: A003
return
@property
def root_real(self) -> Path:
return self.server.root_real # type: ignore[attr-defined]
def _handle_method(self, send_body: bool) -> None:
start = time.perf_counter()
status = 500
size_sent = 0
try:
if self.command not in {"GET", "HEAD"}:
self._send_text(405, "Method Not Allowed")
status = 405
size_sent = 23 if send_body else 0
return
req_path = urlsplit(self.path).path
target = self._resolve_path(req_path)
if not target.exists():
self._send_text(404, "Not Found")
status = 404
size_sent = 14 if send_body else 0
return
if target.is_dir():
index_html = target / "index.html"
index_htm = target / "index.htm"
if index_html.is_file():
target = index_html
elif index_htm.is_file():
target = index_htm
else:
if self.server.no_listing: # type: ignore[attr-defined]
self._send_text(403, "Directory Listing Disabled")
status = 403
size_sent = 31 if send_body else 0
return
status, size_sent = self._send_directory_listing(req_path, target, send_body)
return
if not target.is_file():
self._send_text(403, "Forbidden")
status = 403
size_sent = 14 if send_body else 0
return
status, size_sent = self._send_file(target, send_body)
except PermissionError:
self._send_text(403, "Forbidden")
status = 403
size_sent = 14 if send_body else 0
except Exception:
self._send_text(500, "Internal Server Error")
status = 500
size_sent = 26 if send_body else 0
finally:
elapsed_ms = (time.perf_counter() - start) * 1000.0
self._write_access_log(status, size_sent, elapsed_ms)
def _resolve_path(self, req_path: str) -> Path:
decoded = unquote(req_path)
joined = self.root_real / decoded.lstrip("/")
resolved = Path(os.path.realpath(joined))
root_str = str(self.root_real)
resolved_str = str(resolved)
if resolved_str != root_str and not resolved_str.startswith(root_str + os.sep):
raise PermissionError("Traversal blocked")
return resolved
def _send_text(self, code: int, text: str) -> None:
body = f"{code} {text}\n".encode("utf-8")
self.send_response(code)
self._add_cors_headers()
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def _send_directory_listing(self, req_path: str, directory: Path, send_body: bool) -> tuple[int, int]:
entries = sorted(directory.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
path_display = req_path if req_path.endswith("/") else req_path + "/"
rows = []
if path_display != "/":
parent = str(Path(path_display.rstrip("/")).parent).replace("\\", "/")
if not parent.endswith("/"):
parent += "/"
rows.append('<tr><td>[UP]</td><td><a href="%s">..</a></td><td>-</td><td>-</td></tr>' % html.escape(parent))
for entry in entries:
stat = entry.stat()
name = entry.name + ("/" if entry.is_dir() else "")
href = (path_display.rstrip("/") + "/" + entry.name).replace("//", "/")
if entry.is_dir():
href += "/"
rows.append(
"<tr>"
f"<td>{icon_for(entry.name, entry.is_dir())}</td>"
f'<td><a href="{html.escape(href)}">{html.escape(name)}</a></td>'
f"<td>{'-' if entry.is_dir() else human_size(stat.st_size)}</td>"
f"<td>{dt.datetime.fromtimestamp(stat.st_mtime).isoformat(sep=' ', timespec='seconds')}</td>"
"</tr>"
)
html_body = (
"<!doctype html><html><head><meta charset='utf-8'>"
f"<title>Index of {html.escape(path_display)}</title>"
"<style>body{font-family:sans-serif;margin:1.2rem}table{border-collapse:collapse;width:100%}"
"td,th{border-bottom:1px solid #ddd;padding:0.5rem;text-align:left}</style></head><body>"
f"<h1>Index of {html.escape(path_display)}</h1>"
"<table><thead><tr><th>Type</th><th>Name</th><th>Size</th><th>Modified</th></tr></thead>"
f"<tbody>{''.join(rows)}</tbody></table></body></html>"
).encode("utf-8")
self.send_response(200)
self._add_cors_headers()
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(html_body)))
self.end_headers()
if send_body:
self.wfile.write(html_body)
return 200, len(html_body) if send_body else 0
def _parse_range(self, range_header: str, size: int) -> Optional[tuple[int, int]]:
if not range_header.startswith("bytes="):
return None
spec = range_header[len("bytes=") :]
if "," in spec:
return None
start_s, _, end_s = spec.partition("-")
try:
if start_s == "" and end_s == "":
return None
if start_s == "":
suffix_len = int(end_s)
if suffix_len <= 0:
return None
start = max(0, size - suffix_len)
end = size - 1
else:
start = int(start_s)
end = int(end_s) if end_s else size - 1
if start < 0 or end < start or start >= size:
return None
if end >= size:
end = size - 1
return start, end
except ValueError:
return None
def _send_file(self, file_path: Path, send_body: bool) -> tuple[int, int]:
stat = file_path.stat()
size = stat.st_size
etag = etag_for_file(file_path)
last_modified = email.utils.formatdate(stat.st_mtime, usegmt=True)
if_none_match = self.headers.get("If-None-Match")
if_modified_since = self.headers.get("If-Modified-Since")
if if_none_match == etag:
self.send_response(304)
self._add_cors_headers()
self.send_header("ETag", etag)
self.send_header("Last-Modified", last_modified)
self.send_header("Cache-Control", f"public, max-age={self.server.cache_max_age}") # type: ignore[attr-defined]
self.end_headers()
return 304, 0
if if_modified_since:
try:
ims_dt = email.utils.parsedate_to_datetime(if_modified_since)
file_dt = dt.datetime.fromtimestamp(stat.st_mtime, tz=dt.timezone.utc)
if ims_dt and ims_dt.tzinfo and ims_dt >= file_dt:
self.send_response(304)
self._add_cors_headers()
self.send_header("ETag", etag)
self.send_header("Last-Modified", last_modified)
self.send_header("Cache-Control", f"public, max-age={self.server.cache_max_age}") # type: ignore[attr-defined]
self.end_headers()
return 304, 0
except Exception:
pass
range_header = self.headers.get("Range")
start = 0
end = size - 1
status = 200
if range_header:
parsed = self._parse_range(range_header, size)
if not parsed:
self.send_response(416)
self._add_cors_headers()
self.send_header("Content-Range", f"bytes */{size}")
self.end_headers()
return 416, 0
start, end = parsed
status = 206
ctype, _ = mimetypes.guess_type(str(file_path))
if not ctype:
ctype = "application/octet-stream"
content_len = end - start + 1
self.send_response(status)
self._add_cors_headers()
self.send_header("Content-Type", ctype)
self.send_header("Accept-Ranges", "bytes")
self.send_header("ETag", etag)
self.send_header("Last-Modified", last_modified)
self.send_header("Cache-Control", f"public, max-age={self.server.cache_max_age}") # type: ignore[attr-defined]
self.send_header("Content-Length", str(content_len))
if status == 206:
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
self.end_headers()
if not send_body:
return status, 0
sent = 0
with file_path.open("rb") as fh:
fh.seek(start)
remaining = content_len
while remaining > 0:
chunk = fh.read(min(1024 * 1024, remaining))
if not chunk:
break
self.wfile.write(chunk)
sent += len(chunk)
remaining -= len(chunk)
return status, sent
def _add_cors_headers(self) -> None:
cors = self.server.cors_origin # type: ignore[attr-defined]
if cors:
self.send_header("Access-Control-Allow-Origin", cors)
self.send_header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS")
self.send_header("Access-Control-Allow-Headers", "*")
def _write_access_log(self, status: int, size: int, elapsed_ms: float) -> None:
remote = self.client_address[0] if self.client_address else "-"
if remote.startswith("::ffff:"):
remote = remote[7:]
ts = clf_timestamp(dt.datetime.now().astimezone())
line = (
f'{remote} - - [{ts}] "{self.command} {self.path} HTTP/{self.request_version.split("/", 1)[-1]}" '
f"{status} {size} {elapsed_ms:.2f}ms"
)
print(line)
log_fp: Optional[TextIO] = self.server.log_fp # type: ignore[attr-defined]
if log_fp:
log_fp.write(line + "\n")
log_fp.flush()
def main() -> int:
args = parse_args()
if args.directory:
root = Path(args.directory).expanduser().resolve()
else:
root = Path.cwd() / "sample_static_site"
ensure_sample_directory(root)
if not root.exists() or not root.is_dir():
print(f"Error: directory does not exist or is not a directory: {root}", file=sys.stderr)
return 1
log_fp: Optional[TextIO] = None
if args.log_path:
log_fp = open(args.log_path, "a", encoding="utf-8")
class Server(ThreadingHTTPServer):
root_real = root.resolve()
no_listing = args.no_listing
cache_max_age = args.cache
cors_origin = args.cors
log_fp = log_fp
try:
httpd = Server((args.host, args.port), StaticHandler)
except OSError as exc:
if exc.errno in (98, 10048):
print(f"Error: port {args.port} is already in use.", file=sys.stderr)
elif exc.errno in (13, 10013):
print(f"Error: permission denied binding {args.host}:{args.port}.", file=sys.stderr)
else:
print(f"Error: {exc}", file=sys.stderr)
if log_fp:
log_fp.close()
return 1
if args.ssl:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=args.cert, keyfile=args.key)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
proto = "https" if args.ssl else "http"
print("Static HTTP File Server started")
print(f"URL: {proto}://{args.host}:{args.port}")
print(f"Serving directory: {root}")
print(f"Directory listing: {'disabled' if args.no_listing else 'enabled'}")
print(f"Caching max-age: {args.cache}")
print(f"CORS: {'enabled (' + args.cors + ')' if args.cors else 'disabled'}")
print(f"TLS: {'enabled' if args.ssl else 'disabled'}")
if not args.directory:
print(
f"Sample URLs: {proto}://{args.host}:{args.port}/ "
f"{proto}://{args.host}:{args.port}/assets/style.css "
f"{proto}://{args.host}:{args.port}/scripts/app.js "
f"{proto}://{args.host}:{args.port}/sample.svg "
f"{proto}://{args.host}:{args.port}/hello.txt"
)
should_stop = False
def handle_sigint(signum, frame): # type: ignore[no-untyped-def]
nonlocal should_stop
if not should_stop:
should_stop = True
print("\nReceived SIGINT, shutting down gracefully...")
httpd.shutdown()
signal.signal(signal.SIGINT, handle_sigint)
try:
httpd.serve_forever()
finally:
httpd.server_close()
if log_fp:
log_fp.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())