feat(error): styled themed error page for AppError::IntoResponse (LC-220) #263
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-220-styled-error-page"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
User on staging: hitting a deleted-room URL like
/room/9999while logged in showed raw "Not Found" text on a blank white page in dark mode, no chrome, no theme, no back link. The router fallback for unmatched paths rendered the stylednot_found.html, but every handler-returnedAppErrorreturned bare text.AppError::IntoResponsenow renders a themed standalone error page that extendsbase.htmldirectly. The no-flash theme bootstrap + branding apply automatically; the user sees a card-style page in their chosen theme with a "Back to home" link.What ships
server/templates/error.html— standalone page extendingbase.html, themed via tokens.server/src/views/error_page.rs—ErrorPageview struct.server/src/error.rs—render_styled(status, heading_key, message)helper;AppError::IntoResponserewritten to use it for every variant exceptRedirect.server/locales/{en,es}/common.ftl(8 status headings +error-back-home).Behavior changes
AppError::Internal(msg): previously surfaced "Internal Server Error" as the body. Now renders the styled page WITHOUTmsg. The server-side detail still logs viatracing::error!but never reaches the response body. Closes a mild leak where a sqlx / askama error string could land in the response.AppError::TooManyRequests:Retry-Afterheader still emitted; body now styled.AppError::Conflict / BadRequest / PayloadTooLarge: dynamicmsgstill surfaced as the page detail (server-curated, contextually useful).AppError::Redirect: unchanged.Scope carve-out
Sidebar chrome is deliberately NOT included.
IntoResponsehas noAppStateaccess (no DB pool). The router-levelhandle_not_foundkeeps its sidebar-bearing rendering for the genuinely-unmatched-path 404 case; this commit covers handler-returned errors only. The user complaint was about BARE-TEXT-IN-DARK-MODE; that's fixed end-to-end. A follow-up could thread state through a tower middleware to add chrome on top of these pages.Test plan
cargo check -p lets-chat-serverclean (standalone + saas).cargo clippy --workspace --all-targets --features standalone -- -D warningsclean.cargo fmt --allno-op after fmt run.routes_room_rbac(3 forbidden-path assertions) green./room/{deleted-id}shows themed page in dark mode (NOT bare-text-on-white). Internal error surfaces a card with "Server error" heading, NO sqlx detail text.Pre-existing main red
cargo test --test lc77_webhook_render_fixturefails on bare main (4 fixture mismatches). Independent of LC-220; fixtures needFIXTURE_WRITE=1regenerate via direct docker. Tracked separately.User on staging: hitting a deleted-room URL like `/room/9999` while logged in showed raw "Not Found" text on a blank white page in dark mode, no chrome, no theme, no back link. Same for every other variant of `AppError::IntoResponse`. The router fallback `handle_not_found` rendered the styled `not_found.html` for unmatched paths, but every handler-returned `AppError` returned bare text. Replace the bare-text branches with a themed standalone error page that extends `base.html` directly. The no-flash theme bootstrap and operator branding apply automatically; the user sees a card-style page in their chosen theme with a large "Back to home" link. New surfaces: - `server/templates/error.html`: extends `base.html`, renders status + heading + optional message + back link. Uses `bg-surface text-content` so it themes under all four palettes (light / dark / hc-light / hc-dark). - `server/src/views/error_page.rs`: `ErrorPage` view struct with status, status_heading, message, back_url, back_label, asset_version. - `server/src/error.rs`: `render_styled(status, heading_key, message)` helper renders the page via Askama. `AppError::IntoResponse` now branches per variant on which Fluent key to use and which `msg` (if any) to surface as the body detail. Localized headings + back label added to `server/locales/{en,es}/common.ftl`: `error-status-{not-found,forbidden,unauthorized,conflict,bad-request,payload-too-large,too-many-requests,internal}` plus `error-back-home`. Fluent's `CURRENT_LOCALE` task-local is still in scope when `into_response` runs (set by the `resolve_locale` middleware that wraps the whole handler). Behavior changes worth noting: - `AppError::Internal(msg)`: previously surfaced "Internal Server Error" as the body. Now renders the styled page WITHOUT the `msg` (server-side detail logged via `tracing::error!` as before but never reaches the response body). This closes a mild leak where a sqlx / askama error string would land in the response body. - `AppError::TooManyRequests(msg, retry_after)`: the `Retry-After` header is still emitted; the body now renders the styled page. Operators with well-behaved HTTP clients see no change in back-off behavior. - `AppError::Conflict / BadRequest / PayloadTooLarge`: the dynamic `msg` IS still surfaced as the page detail, because those messages are server-curated and contextually useful to the user (e.g. "unknown quote_id", "burst exceeds 10000"). Sidebar chrome is deliberately NOT included. The IntoResponse path has no `AppState` access (no DB pool to load chrome from). The router-level `handle_not_found` keeps its sidebar-bearing rendering for the genuinely-unmatched-path 404 case; this commit covers handler-returned errors only. A follow-up could thread state through a tower middleware to add chrome on top of these pages, but the user complaint was the BARE-TEXT-IN-DARK-MODE failure, which this commit fixes end-to-end. Verified: `cargo check -p lets-chat-server` clean (standalone + saas), `cargo clippy --workspace --all-targets --features standalone -- -D warnings` clean, `cargo fmt --all` no-op after fmt run, smoke test on `routes_room_rbac` (3 forbidden-path assertions) green. #LC-220