Crash fix and request logging

The problem

Serving the references/ folder with penerbit, requesting large markdown files caused two failures:

  1. mastering-nim.md (315KB) — HTTP 500 with empty body. Server survived but gave the client nothing useful.
  2. mastering-nim_raw.md (587KB) — Full process crash. Server died completely.

Both looked like a memory leak but were actually unhandled exceptions from the markdown library (v0.8.8).

Root cause

Bug 1: IndexDefect (315KB file)

The markdown library's parseInlineLink hits an out-of-bounds array access on malformed inline link syntax in the OCR'd text:

index 43 not in 0 .. 42

In Nim, IndexDefect inherits from Defect, not CatchableError. The original code had no try/except at all, but even except CatchableError would miss it. Only except Exception catches both families.

Bug 2: Stack overflow (587KB file)

The markdown library recursively processes nested lists via parseBlock -> finalizeList -> parseBlock. With the deeply nested structure in the raw OCR text, this recursion exceeds 2000 frames. In debug builds, Nim's call depth limit fires — but it fires inside alloc.nim mid-allocation, so the stack is too corrupted to unwind. The process dies before any except clause can run.

This is fundamentally uncatchable. The only defense is not calling markdown() on files this large.

What was missing

The original handler at penerbit.nim:108 was a naked markdown(content) call with:

Changes

src/penerbit.nim

log(level, msg) — Thread-safe timestamped logging via echo. Nim's std/logging module uses GC'd globals that aren't safe in mummy's worker threads, so this uses plain echo which is line-buffered and thread-safe.

logRequest template — Emits a structured log line for every request:

08:48:48 INF GET /nim-manual.md 200 346207B 3909.7ms

renderMarkdown(filePath, reqPath) — Centralizes all markdown rendering with three layers of protection:

  1. Size guard (maxMarkdownSize = 512KB) — Files over the limit skip markdown parsing entirely and are served as raw <pre> text. Logs a WRN.
  2. except Exception — Catches both CatchableError and Defect (like IndexDefect). Returns a styled 500 error page. Logs an ERR with the exception type and file size.
  3. Timing — Logs how long markdown parsing took per file.

sendResponse helper — Reduces repeated header boilerplate across all response paths.

src/style.nim

render500(path, msg) — Styled error page matching the existing 404 page design. Shows the path that failed and the error message.

nim.cfg

-d:nimCallDepthLimit=10000 — Raised from the default 2000. Defense-in-depth so that moderately deep recursion from the markdown library can be caught by the except clause rather than killing the process.

Server log output

Before (nothing):

penerbit serving /home/pengacau/penerbit/references
  http://localhost:8080/

After:

penerbit serving /home/pengacau/penerbit/references
  http://localhost:8080/
08:48:48 INF GET / 200 4719B 4.1ms
08:48:51 ERR /mastering-nim.md | markdown failed (314763B): [IndexDefect] index 43 not in 0 .. 42
08:48:51 INF GET /mastering-nim.md 500 3907B 3199.8ms
08:48:51 WRN /mastering-nim_raw.md | skipped markdown render (587490B > 524288B limit)
08:48:51 INF GET /mastering-nim_raw.md 200 591222B 1.3ms
08:48:51 INF GET /nonexistent 404 3778B 0.1ms
08:48:51 INF GET / 200 4719B 8.5ms

Verification

nimble dev
bin/penerbit references
# In another terminal:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/mastering-nim.md    # 500, server alive
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/mastering-nim_raw.md # 200, raw fallback
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/                    # 200, still alive