The previous fix only covered migration errors. The same truncation
affected two more places, both because tokio_postgres::Error's Display
prints only "db error" while the real message lives in source():
- api/error.rs: AppError::{db_conn,query,tx} logged with {e}, so every
runtime DB failure showed as "Query failed: db error". The
constructors' bound is tightened from impl Display+Debug to
impl std::error::Error so the source chain can be walked; the
user-facing message stays sanitized (existing tests still pass).
- db/pool.rs: ensure_database formatted tokio_postgres::Error directly
into the String returned to main.rs' exit(1) path, collapsing to
"failed to query pg_database: db error" etc.
Adds a shared crate::db::format_with_sources helper (walks source()
with de-dup of placeholder layers) and reuses it from MigrateError's
Display, the AppError constructors, and pool.rs.
tokio_postgres::Error's Display prints only the placeholder "db error"
for DB-side failures; the actual message (e.g. column already exists,
SQLSTATE, constraint name) lives in source() as DbError. Without
traversing the chain, startup logs showed the useless
"migration 001 failed: db error", making failures impossible to
diagnose.
MigrateError::Display now appends each source layer with ": " and
exposes source() so error-reporting layers that walk the chain
themselves also benefit.
DB migration failure at startup panicked via .expect() (main.rs:223),
dumping a raw backtrace for what is usually just "PostgreSQL isn't running
yet". Worse, the 1.6s runtime retry budget was reused at startup, so it
almost always failed against a cold/slow DB (docker-compose without
healthchecks, local Postgres not started, etc.).
Changes:
- pool: extract build_pg_config() (returns Result, no panic); add
validate_database_url() so URL-format / DB_POOL_SIZE errors surface as
friendly exit(1) instead of hitting the LazyLock's unreachable .expect()
- pool: add get_conn_for_startup() — a startup-only retry loop with a
configurable total-duration window (MIGRATE_STARTUP_TIMEOUT_SECS,
default 30s, 500ms interval), separate from the runtime get_conn()
anti-avalanche fast retry (untouched)
- migrate: split run() into run_on_conn(&mut conn) so callers control the
connection-acquisition strategy; main.rs pairs this with the startup
retry. Advisory-lock release comment updated: exit(1) terminates the
process just like panic, so the session-level lock is still freed
- main: startup fatal errors (bad URL, DB unreachable, migration failed)
now each emit tracing::error! + eprintln! ERROR + targeted HINT, then
exit(1) — no panic, no backtrace nudge
- .env.example / AGENTS.md: document MIGRATE_STARTUP_TIMEOUT_SECS
No runtime behavior change: get_conn() and its ~40 call sites are
unchanged. Advisory-lock safety preserved (documented in migrate.rs).
P0 blockers:
- Fix migration numbering conflict and duplicate indexes
- Change comments.post_id FK to ON DELETE CASCADE
- Restrict public post detail endpoint to published posts only
- Fix rate-limiting IP extraction and fallback to ConnectInfo
- Harden HTML sanitizer: deny unknown URL schemes, restrict data URIs
- Remove session token from login response body
- Enforce image pixel/dimension limits on upload and serving
P1 high-risk:
- Validate uploads by magic bytes and decode GIF/WebP
- Add pagination/rate-limiting to search, tag posts, and comments
- Make first-admin registration and slug uniqueness check atomic
- HTML-escape comment author fields
- Improve HTML minify cache key and skip admin/error responses
- Add mobile navigation menu
P2 accessibility/quality:
- Associate form labels with inputs
- Key PostDetail article by slug to re-init scripts on navigation
- Improve image viewer keyboard accessibility
- Make theme toggle SSR-friendly and add aria-label
- Invalidate slug 404 cache on create and pending count on new comment
- Deduplicate tags case-insensitively
P3 cleanup:
- Remove unused tower-http dependency, expand make clean
- Configure DB pool timeouts and verified recycling
- Run background cleanup tasks immediately on startup
- Use SHA-256 for stable disk cache keys
- Log DB errors with Display instead of Debug
- Update README migration instructions
All tests pass (321), clippy clean, dx check clean.
- Cache ammonia::Builder with LazyLock (was rebuilt per request)
- Enable tracing release_max_level_info to strip tracing overhead at compile time
- Remove TraceLayer and tower-http trace feature from production
- Increase DB pool size 10→20 (configurable via DB_POOL_SIZE)
- Increase SSR cache TTL 300s→3600s (configurable via SSR_CACHE_SECS)
Benchmark: 7,444 → 9,840 req/s, P99 latency 27.6ms → 11.1ms
- Merge unnecessary line splits in db/pool.rs and pages/admin.rs
- Prefix unused variable with underscore in login.rs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>