soul.demarkus.io/debugging.md/v4 draft reader meta

Debugging

Lessons learned from bugs, investigations, and things that went wrong.

General Principles

  • Read the error message. The whole error message.
  • Reproduce before fixing. If you can't trigger it, you don't understand it.
  • Don't brute force. If something is blocked, step back and think about why.
  • When a test fails, read the test first — the test might be wrong.

Known Gotchas

YAML Auto-Typing

YAML will silently convert strings like yes, no, on, off to booleans. This is why frontmatter is parsed as map[string]string — we control the type conversion ourselves.

Symlink Resolution in Tests

When testing the versioned store, be careful with os.ReadFile vs following symlinks. The store creates doc.md -> versions/doc.md.v3 — some operations need the symlink target, others need the symlink itself.

Path Traversal

filepath.Clean handles most cases but you still need an explicit .. check after cleaning. A path like /../../../etc/passwd cleans to /etc/passwd which looks valid but is outside the content root. The check is: clean the path, then verify it doesn't escape the root.

QUIC Stream Lifecycle

Streams must be closed properly. If a handler panics or returns without closing the stream, the client hangs waiting for data. Always defer stream close.

Debugging Approach for This Project

  1. Write a failing test that reproduces the issue
  2. Fix the code
  3. Verify the test passes
  4. Check if the fix breaks anything else (make test)
  5. Run bash pre-commit.sh

The test-first approach isn't dogma here — it's practical. A test that reproduces the bug proves you understand the bug, and it prevents regression.

QUIC UDP Buffer Size Warning

When running demarkus in CI or containerized environments, quic-go logs this warning:

failed to sufficiently increase receive buffer size (was: 1024 kiB, wanted: 7168 kiB, got: 2048 kiB)

Not a failure. quic-go tries to increase the UDP receive buffer to 7168 kiB for optimal performance. In environments with restricted sysctl limits, it can only get partway there. The connection still works — everything publishes fine.

Fix (if needed): Increase the system limit with sysctl -w net.core.rmem_max=7168000. Not necessary for correctness, only for optimal throughput under heavy load.

First seen: CI publishing content to demarkus-hub (hub.demarkus.io).

OnNode Callback Concurrency

graph.Crawl invokes OnNode from multiple worker goroutines. Any mutable state captured by the callback must use sync/atomic or a mutex. The CrawlAndPersist MaxNodes counter uses atomic.Int32 for this reason. Always run go test -race on packages that use OnNode.

Glamour WithAutoStyle() Blocks TUI Rendering

Symptom: TUI shows "Loading..." for up to 60 seconds after fetch completes. Server logs confirm the FETCH is handled immediately. CLI works fine. A mouse scroll "unsticks" the display.

Root cause: glamour.WithAutoStyle() calls termenv.HasDarkBackground(), which sends an OSC 11 escape sequence to the terminal and reads the response from stdin. But Bubbletea is also reading stdin for input events. This causes two problems:

  1. Stdin contention — Bubbletea's input goroutine may consume the OSC 11 response, so termenv never sees it and blocks for the full 5-second timeout.
  2. Render corruption — The stdin race breaks Bubbletea's render cycle, leaving the view stuck on "Loading..." even though the model state has been updated. A mouse event forces a fresh render, which is why scrolling "fixes" it.

The TUI recreated the glamour renderer on every window resize (to update word wrap width), re-triggering the query each time. Multiple resize events at startup compounded into 30-60 seconds of blocking.

Fix: Detect dark/light background once in main() before tea.NewProgram().Run() takes over stdin. Pass the resolved style name ("dark" or "light") into the model. Use glamour.WithStandardStyle(styleName) instead of glamour.WithAutoStyle() — this hits the DefaultStyles map lookup directly, bypassing the terminal query entirely. Also added a -style flag so users can skip detection.

Guard: Both stdin and stdout must be terminals before attempting detection. If either is piped, fall back to dark.

Key insight: Never do terminal escape sequence queries inside a Bubbletea event loop. Bubbletea owns stdin once it starts. Any library that reads stdin for terminal responses will race with Bubbletea's input goroutine.

trail
  1. soul.demarkus.io v4