Error Handling & a Real Development Server With Live Reload
Building a Web Framework
Chapter 8 ยท Error Handling & a Real Development Server With Live Reload
Two honest gaps have been sitting open since earlier chapters. Chapter 5 verified that a real, uncaught exception reaches the client as wsgiref's own generic "A server error occurred. Please contact the administrator." — real, working, and completely uninformative. Chapter 4 verified that editing a cached template's file on disk changes nothing a running server serves, until the process itself restarts. This chapter closes both: a real debug error page, and a real file-watcher that reloads what it needs to without a human ever typing a restart command.
A Real Debug Middleware
Exactly the same shape as Chapter 5's own error_handling_middleware — a try/except around next_() — but instead of returning one fixed message, it builds a real traceback from Python's own standard library:
App gets one new attribute — a real, explicit debug flag, mirroring the identical real convention Flask and Django both use:
ValueError raised deep inside a handler, hit against App(debug=True), returns a real 500 whose body genuinely contains the actual source file, the actual line numbers, and the exact exception message — "something genuinely went wrong, deep in a real handler", confirmed present verbatim. The identical crash against App(debug=False) returns the short, safe, generic message instead — deliberately built by this chapter's own code now, not merely inherited from wsgiref's own fallback.
The Traceback Itself Needs the Exact Same Discipline as Chapter 4
A traceback is still just text dropped into an HTML response — and an exception's own message is real, attacker-influenced data whenever it's built from user input. html.escape(tb_text) above isn't decoration:
make_debug_middleware, the client's own response body contains ValueError: <script>alert(1)</script> — confirmed directly that the literal, executable <script> tag is genuinely absent from the real response, with only its escaped form present. Skip the html.escape() call and this debug page becomes a second, real XSS vector, structurally identical to Chapter 4's own original finding — just fed by an exception message instead of a template variable.
A Real File Watcher
Every file on disk carries a real, OS-tracked last-modified time. Comparing it against a previously recorded value is enough to detect a genuine edit, with no need to read or hash the file's own contents:
changed() called right after construction, with no edit yet, returns None. A real write to the watched file, followed by another changed() call, returns that file's own real path. Calling changed() a second time immediately afterward, with no further edit, correctly returns None again — the stored mtime was updated the moment the change was reported, so the same edit is never reported twice.
Resolving Chapter 4's Own Stale-Cache Finding
A watcher on the template directory, checked once at the top of every request, is enough to invalidate TemplateEngine's own cache exactly when it's genuinely stale:
Hello, Ada! (v1). The template file is then genuinely edited on disk while the server keeps running — the identical real setup Chapter 4 used to demonstrate the problem. Request 2, this time, returns Hi there, Ada! (v2, updated on disk): the new content, correctly, on the very next request — no restart needed.
Reloading the App's Own Source, Not Just Templates
A template can be recompiled in place because TemplateEngine owns it directly. A route handler, a middleware, or the framework's own source files can't be swapped out mid-process the same way — genuinely picking up a change there needs the whole Python process to start over. Werkzeug's own real reloader (the one Flask's debug=True uses) solves this with os.execv(): replacing the running process's own program image in place.
Verified with a real, separately spawned subprocess — running this exact loop, watching one real file, with a small marker file recording its own restart count and PID on every start:
restart_count increased from a real 1 to a real 2, confirmed by reading the marker file back from outside the subprocess entirely — not inferred, not simulated. A single real file edit, made while the subprocess kept running, was enough to trigger it.
os.execv() replaces the current process's own program image while keeping the identical process ID — the whole reason it's called "exec," not "spawn." Verified directly on this real Windows machine, the PID recorded in the marker file changed across the reload, 32112 → 38476, rather than staying fixed. Windows has no true POSIX exec() syscall for Python to call into, so its own emulation genuinely starts a new process rather than replacing the current one in place. The visible, real effect — the app's own code restarting itself from scratch — is identical either way; only the specific mechanism, and the resulting PID, differ by platform.
Where This Course Is Headed
Chapter 9 builds real static file serving and a small command-line tool for the framework itself — likely the actual entry point that wires debug=True and run_with_reloader() together into one real python -m myframework runserver command.
Hands-On Exercises
Extend this chapter's own make_debug_middleware() to include the real request's method and path (e.g., "GET /widgets/42") in the debug page's own body, correctly HTML-escaped, and verify against a real live request to a dynamic route that the exact real method and path genuinely appear in the client's own response.
๐ View solutionConstruct a real FileWatcher watching two genuinely separate files, edit only the first one and verify changed() reports exactly that file's own path (not the second one), confirm a second call to changed() returns None, then edit only the second file and verify changed() now correctly reports that one instead.
๐ View solutionWire this chapter's own FileWatcher-based template-reload check so it only runs when debug is True, and verify live, against two real Apps (one debug=True, one debug=False), that editing a template file mid-run is picked up automatically in the debug=True case but genuinely stays stale in the debug=False case โ explaining in your own words why skipping a real filesystem stat() call on every single request is a real, worthwhile thing to avoid in production.
๐ View solutionChapter 8 Quick Reference
- Debug middleware โ try/except around next_(), building a real traceback.format_exc() body when App.debug is True, a short generic message otherwise
- The traceback must be escaped โ verified as a real, live reflected-XSS risk otherwise, closed with html.escape(), the identical discipline Chapter 4 already established
- FileWatcher โ mtime-based change detection, verified reporting a real edit exactly once, never re-reporting an already-consumed change
- Fixing Chapter 4's own stale-cache finding โ clearing TemplateEngine._cache when the watcher fires, verified serving the real, updated content on the very next request
- run_with_reloader() โ a real os.execv()-based restart, verified with a genuinely spawned subprocess and a marker file showing restart_count actually increasing
- A real, verified Windows-specific finding โ the PID changed across the reload here, unlike POSIX's own documented same-PID exec() behavior; the visible restart still worked identically
- Next chapter: Static files & a real CLI for the framework itself