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:

import traceback, html def make_debug_middleware(app): def middleware(request, next_): try: return next_(request) except Exception: tb_text = traceback.format_exc() if app.debug: body = ( '<h1>500 Internal Server Error</h1>' f'<pre>{html.escape(tb_text)}</pre>' ) return Response(body, status=500, content_type='text/html') return Response('500 Internal Server Error', status=500) return middleware

App gets one new attribute — a real, explicit debug flag, mirroring the identical real convention Flask and Django both use:

class App: def __init__(self, debug=False): self.router = Router() self.middlewares = [] self._chain = None self.debug = debug # ... everything else unchanged from Chapter 5 ...
Verified directly — a real, useful traceback reaches the client, only when debug is genuinely on
A real 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:

def crashing_with_html_handler(request): raise ValueError('<script>alert(1)</script>')
Verified directly — a real, live reflected-XSS risk, and confirmation it's closed
Raised for real and rendered through make_debug_middleware, the client's own response body contains ValueError: &lt;script&gt;alert(1)&lt;/script&gt; — 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:

import os class FileWatcher: def __init__(self, paths): self.paths = list(paths) self._mtimes = {p: self._safe_mtime(p) for p in self.paths} def _safe_mtime(self, path): try: return os.stat(path).st_mtime except FileNotFoundError: return None def changed(self): for p in self.paths: current = self._safe_mtime(p) if current != self._mtimes[p]: self._mtimes[p] = current # update the stored value -- this change is now "consumed" return p return None
Verified directly — a real edit detected, and correctly not re-reported
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:

watcher = FileWatcher(['templates/greeting.html']) def app(environ, start_response): if watcher.changed(): templates._cache.clear() # force every template to recompile on next access body = templates.render('greeting.html', name='Ada').encode('utf-8') # ...
Verified directly — the exact scenario from Chapter 4, now fixed live
Request 1, before any edit: 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.

import sys, time def run_with_reloader(watched_paths): watcher = FileWatcher(watched_paths) while True: time.sleep(0.1) if watcher.changed(): # genuinely re-exec this process -- a real, working reload, not a simulation os.execv(sys.executable, [sys.executable] + sys.argv)

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:

marker state right after startup: {'restart_count': 1, 'pids': [32112]} # ... a real edit is made to the watched file while the subprocess is running ... marker state after the real edit + reload window: {'restart_count': 2, 'pids': [32112, 38476]}
Verified directly — a real, live subprocess genuinely restarted itself
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.
A genuine, directly-observed platform difference — not the behavior POSIX documents
On a true POSIX system, 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

Exercise 1

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 solution
Exercise 2

Construct 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 solution
Exercise 3

Wire 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 solution

Chapter 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