Adding a Debug View for FastHTML Learning
mini-projects
Thinking about how to teach web dev and FastHTML inspired this quick mini-project: making a debug view for FastHTML apps that renders requests and responses. Nothing fancy, but with minimal work and a bit of MonsterUI for styling I think it looks and works quite well!
Here’s how this is used in this demo app:
from fasthtml.common import *
from debug import debug_wrap
= FastHTML()
app
debug_wrap(app)= app.route
rt
@rt('/')
def get(): return Div(
'Hello World!'),
P('About', href='/about'),
A(id='htmxtest'),
Div('Click me', hx_post='/test', hx_target='#htmxtest'))
Button(
@rt
def about(a:int=None):
return Titled('About',
'This is the about page'),
P('Go back', href="/"),
A(f'You passed a={a}') if a else None
P(
)
@rt('/test')
def post(): return Div('Clicked!')
serve()
And the entirity of debug.py:
from fasthtml.common import *
from collections import deque
from monsterui.all import *
from typing import Any
import json, pprint, textwrap
= deque(maxlen=10)
updates
def before(req, session):
if '/debug' in str(req.url): return
updates.append({'type': 'request',
'method': req.method,
'url': str(req.url),
'session': dict(session or {}),
'headers': dict(req.headers)
})
def after(resp):
= to_xml(resp)
resp_html if not resp or 'debug' in resp_html: return
'type': 'response','html': resp_html})
updates.append({
def debug_wrap(app):
app.before.append(before)
app.after.append(after)
@app.route('/debug')
def debug_page():
return Div(
"Debugging Console", cls=TextFont.bold_sm),
H3("Requests/responses captured:"),
P(id='debug_updates', hx_trigger='every 1s', hx_get='/debug_updates', hx_swap='afterbegin'),
Div(='p-4 space-y-4'
cls=True)
), Theme.orange.headers(highlightjs
@app.route('/debug_updates')
def debug_updates_view():
= []
items while len(updates):
= updates.popleft()
data if data['type'] == 'request' else ResponseCard(data))
items.append(RequestCard(data) if not items: return
return Div(*items, cls='debug')
def RequestCard(data: dict[str, Any]):
return Card(DivVStacked(
"REQUEST", cls=(TextT.bold,)),
H4(f"{data.get('method')}: {data.get('url')}", cls=TextFont.muted_sm),
P(
DivCentered(Grid('Session'), render_md("```js\n"+wrap_pformat(json.dumps(data.get('session')))+"\n```"))),
DivCentered(Details(Summary('Headers'), render_md("```js\n"+wrap_pformat(json.dumps(data.get('headers')))+"\n```"))),
DivCentered(Details(Summary(
))
))
def ResponseCard(data: dict[str, Any]) -> FT:
= data.get('html') or ''
html_str return Card(DivVStacked(
"RESPONSE", cls=(TextT.bold,)),
H4("```html\n"+html_str+"```")))
render_md(
### For formatting json into something I can stuff in a code block, thanks AI:
class WrappingPrettyPrinter(pprint.PrettyPrinter):
def _format(self, obj, stream, indent, allowance, context, level):
# If it's a long string, forcibly wrap it
if isinstance(obj, str) and len(obj) > self._width:
# Break the string into lines of up to self._width
= textwrap.wrap(obj, self._width)
wrapped_lines for i, line in enumerate(wrapped_lines):
if i > 0:
# Move to a new line and indent properly
'\n' + ' ' * indent)
stream.write(super()._format(line, stream, indent, allowance if i == 0 else 1, context, level)
else:
# Otherwise, do the normal pprint formatting
super()._format(obj, stream, indent, allowance, context, level)
def wrap_pformat(obj, width=80):
"""Return a pretty-printed string where long strings are line-wrapped."""
= WrappingPrettyPrinter(width=width)
printer return printer.pformat(obj)