Error Pages
Custom error handling for production mode with support for branded error pages.
Production-Ready Error Pages
Soli provides detailed error pages during development and clean, user-friendly pages in production. You can customize error pages to match your brand.
1 Development vs Production Mode
Development Mode
- Full stack traces
- Interactive REPL for debugging
-
View locals from
render()on template errors - Request details (params, body, headers)
- Source code preview
soli serve
The interactive REPL is token-protected and loopback-only by default. For a trusted local server accessed from another machine, opt in with SOLI_DEV_REPL_ALLOW_REMOTE=1 and a pinned SOLI_DEV_REPL_SECRET — the server refuses to start otherwise (SEC-051), and the secret is never embedded in the HTML error page.
Production Mode
- Clean, branded error pages
- No failure details leaked to the visitor
- Error ID for support reference
- Multi-line stderr block with full context
- Customizable templates
soli serve --no-dev
Inspecting variables in the REPL
The REPL evaluates expressions against a snapshot of the environment captured the moment the request failed. The controller and request state is always present — this (including any @ivars you set), params, req, and cookies. When the error happened while a template was rendering, the data you passed to render(...) is captured too: each key as a top-level name, and the whole hash as _view_data.
def show
@page_owner = "Olivier"
render("posts/show", {
"title": "Hello",
"posts": Post.all()
})
end
# Then, in the error-page REPL:
title # => "Hello"
posts.map(fn(p) p.title) # locals are real values — run expressions, not just reads
_view_data.title # same data, under the _view_data object
this.page_owner # controller instance vars
When a view errored, the page also shows a View Locals panel with a one-click button per render(...) local (plus an All locals button for _view_data), so you don't have to remember the names.
A name passed to render(...) that collides with a controller-scope variable is shadowed by the controller value at the top level — reach the view's copy under _view_data.<key> (the View Locals buttons do this for you). Variables created inside the template (a <% total = ... %> assignment, a loop variable) are not part of the snapshot; only the locals you passed into render(...) are.
Error logging to stderr
For every 500 the server writes a multi-line block to stderr — useful when you only have access to container / journald logs. This fires in both --dev and --no-dev so failures show up in your terminal during development the same way they do in production. The first [ERROR] request_id=… METHOD PATH - msg line is preserved from earlier versions so existing log parsers keep working.
[ERROR] request_id=05dedb29-… GET /users/42 - Type error: cannot index null with string at 7:13
stack:
show at app/controllers/users.sl:92
find at app/models/user.sl:14
request:
{
"body": "[REDACTED]",
"headers": { ... },
"method": "GET",
"params": { ... },
"path": "/users/42",
"query": { ... },
"session": "N/A"
}
env: {"current_user": null, "user_id": null, ...}
Secrets are redacted in the stderr snapshot: Authorization and cookie / token / password params show [REDACTED]; the request body is always redacted. The env: line is filtered by the same rule, so a local variable named like a secret (password, api_key, access_token, …) is logged as [REDACTED] instead of by value. Failure context is written only to stderr — never to the production HTML page.
Correlate a customer's Error ID to a stderr block by searching the logs for request_id=<that id>.
2 Default Error Pages
Soli includes built-in error pages for common HTTP status codes:
| Status | Description |
|---|---|
| 400 | Bad Request |
| 403 | Forbidden |
| 404 | Not Found |
| 405 | Method Not Allowed |
| 500 | Internal Server Error |
| 502 | Bad Gateway |
| 503 | Service Unavailable |
3 Custom Error Pages
Create custom error pages by placing templates in app/views/errors/.
app/
views/
errors/
400.html.slv # Custom 400 Bad Request page
403.html.slv # Custom 403 Forbidden page
404.html.slv # Custom 404 Not Found page
500.html.slv # Custom 500 Internal Server Error page
502.html.slv # Custom 502 Bad Gateway page
503.html.slv # Custom 503 Service Unavailable page
Template Variables
| Variable | Type | Description |
|---|---|---|
| status | Number | HTTP status code (e.g., 500) |
| message | String | Error message |
| request_id | String | Unique identifier for support |
Example: Custom 404 Page
<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa;">
<div style="text-align: center;">
<h1 style="font-size: 6rem; font-weight: bold; color: #343a40; margin-bottom: 1rem;"><%= status %></h1>
<h2 style="font-size: 1.5rem; font-weight: 600; color: #6c757d; margin-bottom: 1rem;">Page Not Found</h2>
<p style="color: #6c757d; margin-bottom: 2rem;"><%= message %></p>
<a href="/" style="display: inline-block; padding: 0.75rem 1.5rem; background-color: #007bff; color: white; border-radius: 0.375rem;">Go Home</a>
<p style="margin-top: 2rem; font-size: 0.75rem; color: #adb5bd;">Error ID: <%= request_id %></p>
</div>
</div>
Example: Custom 500 Page
<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #fff5f5;">
<div style="text-align: center; max-width: 400px;">
<h1 style="font-size: 3rem; font-weight: bold; color: #c53030; margin-bottom: 1rem;"><%= status %></h1>
<h2 style="font-size: 1.25rem; font-weight: 600; color: #1a202c; margin-bottom: 1rem;">Something went wrong</h2>
<p style="color: #4a5568; margin-bottom: 2rem;">We're sorry, but something unexpected happened.</p>
<a href="/" style="display: inline-block; padding: 0.75rem 1.5rem; background-color: #c53030; color: white; border-radius: 0.375rem;">Return to Homepage</a>
<p style="margin-top: 2rem; font-size: 0.75rem; color: #718096;">Reference ID: <%= request_id %></p>
</div>
</div>
If your application already uses Tailwind CSS, you can use Tailwind classes instead of inline styles for cleaner templates.
4 Important Notes
-
No Layouts
Error pages render without application layouts to avoid potential cascading errors.
-
Optional
Custom error pages are optional. If a template doesn't exist, Soli uses the default error page.
-
Full Template Features
Error templates support all template features including conditionals, loops, and partials.
5 Production Deployment
# Using soli CLI
soli serve --no-dev
# Or set environment variable
SOLI_ENV=production soli serve
Production Mode Ensures:
- Custom error pages are used
- Auth headers, tokens, passwords, and request bodies are redacted
- Error IDs are generated for support reference
Best Practices
Keep it Simple
Error pages should be clean and focused on helping users return to working pages.
Provide Navigation
Include links to help users return to working pages (home, back, etc.).
Log Error IDs
Store error IDs in your logs to correlate user reports with server errors.
Test Error Flows
Regularly test custom error pages to ensure they render correctly.