Skip to content

Commit eec739b

Browse files
authored
fix: add custom 404 support to dev server (#94)
1 parent eb37c96 commit eec739b

2 files changed

Lines changed: 91 additions & 3 deletions

File tree

src/rockgarden/cli.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import shutil
55
import socketserver
66
import tomllib
7-
from functools import partial
87
from pathlib import Path
98
from typing import Annotated, NoReturn
109

@@ -201,6 +200,36 @@ def build(
201200
typer.echo(f" {page_slug}: [[{target}]]", err=True)
202201

203202

203+
def _make_handler(
204+
output_dir: Path,
205+
) -> type[http.server.SimpleHTTPRequestHandler]:
206+
"""Create an HTTP handler that serves custom 404.html when present."""
207+
208+
class _Handler(http.server.SimpleHTTPRequestHandler):
209+
def __init__(self, *args: object, **kwargs: object) -> None:
210+
super().__init__(*args, directory=str(output_dir), **kwargs) # type: ignore[arg-type]
211+
212+
def send_error(
213+
self,
214+
code: int,
215+
message: str | None = None,
216+
explain: str | None = None,
217+
) -> None:
218+
if code == 404:
219+
custom_404 = Path(self.directory) / "404.html" # type: ignore[attr-defined]
220+
if custom_404.is_file():
221+
body = custom_404.read_bytes()
222+
self.send_response(404)
223+
self.send_header("Content-Type", "text/html; charset=utf-8")
224+
self.send_header("Content-Length", str(len(body)))
225+
self.end_headers()
226+
self.wfile.write(body)
227+
return
228+
super().send_error(code, message, explain)
229+
230+
return _Handler
231+
232+
204233
@app.command()
205234
def serve(
206235
output: Annotated[
@@ -226,7 +255,7 @@ def serve(
226255
typer.echo("Run 'rockgarden build' first.", err=True)
227256
raise typer.Exit(1)
228257

229-
handler = partial(http.server.SimpleHTTPRequestHandler, directory=str(output_dir))
258+
handler = _make_handler(output_dir)
230259

231260
class ReuseAddrServer(socketserver.TCPServer):
232261
allow_reuse_address = True

tests/test_cli.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import socketserver
12
import tempfile
3+
import threading
4+
import urllib.request
25
from pathlib import Path
36

47
from typer.testing import CliRunner
58

69
from rockgarden import __version__
7-
from rockgarden.cli import app
10+
from rockgarden.cli import _make_handler, app
811

912
runner = CliRunner()
1013

@@ -124,3 +127,59 @@ def test_build_unhandled_error_shows_type_and_message(tmp_path, monkeypatch):
124127
assert result.exit_code == 1
125128
assert "RuntimeError: something broke" in result.output
126129
assert "Traceback" not in result.output
130+
131+
132+
def _start_serve_server(output_dir):
133+
"""Start the real rockgarden handler on an OS-assigned port."""
134+
handler = _make_handler(output_dir)
135+
136+
# Suppress request logging during tests
137+
handler.log_message = lambda self, *a, **kw: None # type: ignore[assignment]
138+
139+
class ReuseAddrServer(socketserver.TCPServer):
140+
allow_reuse_address = True
141+
142+
httpd = ReuseAddrServer(("127.0.0.1", 0), handler)
143+
port = httpd.server_address[1]
144+
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
145+
thread.start()
146+
return httpd, port
147+
148+
149+
def test_serve_custom_404(tmp_path):
150+
output_dir = tmp_path / "_site"
151+
output_dir.mkdir()
152+
(output_dir / "index.html").write_text("<h1>Home</h1>")
153+
(output_dir / "404.html").write_text("<h1>Custom Not Found</h1>")
154+
155+
httpd, port = _start_serve_server(output_dir)
156+
try:
157+
try:
158+
urllib.request.urlopen(f"http://127.0.0.1:{port}/nonexistent")
159+
except urllib.error.HTTPError as e:
160+
assert e.code == 404
161+
body = e.read().decode()
162+
assert "Custom Not Found" in body
163+
else:
164+
raise AssertionError("Expected 404 HTTPError")
165+
finally:
166+
httpd.shutdown()
167+
168+
169+
def test_serve_default_404_without_custom_page(tmp_path):
170+
output_dir = tmp_path / "_site"
171+
output_dir.mkdir()
172+
(output_dir / "index.html").write_text("<h1>Home</h1>")
173+
174+
httpd, port = _start_serve_server(output_dir)
175+
try:
176+
try:
177+
urllib.request.urlopen(f"http://127.0.0.1:{port}/nonexistent")
178+
except urllib.error.HTTPError as e:
179+
assert e.code == 404
180+
body = e.read().decode()
181+
assert "Custom Not Found" not in body
182+
else:
183+
raise AssertionError("Expected 404 HTTPError")
184+
finally:
185+
httpd.shutdown()

0 commit comments

Comments
 (0)