A Rails middleware that provides concise, plain text error output optimized for LLMs and coding agents.
This lets your tool test your application and debug errors without filling up its context window.
For example, if you're using Playwright MCP with Claude Code or another LLM-powered tool, PlainErrors will return simpler error messages.
In my test with a real Rails application, PlainErrors achieves significant
token reductions over both
BetterErrors
and the standard Rails development error page
(as calculated by OpenAI's tiktoken library):
| Metric | PlainErrors | Rails Default | BetterErrors |
|---|---|---|---|
| Bytes | 755 | 8,854 | 113,544 |
| Tokens | 217 | 2,975 (13.7x more) | 25,055 (115.5x more) |
(To be clear, I like BetterErrors and the Rails default error page -- they're great for manual human debugging. They're just not optimized for LLMs or automation workflows.)
Cheat code: just point your AI agent to this README and ask it to install!
Add to your Gemfile:
group :development do
gem 'plain_errors', github: 'panozzaj/plain_errors'
endThen run bundle install.
Add configuration and middleware in an initializer:
# config/initializers/plain_errors.rb
if defined?(PlainErrors)
PlainErrors.configure do |config|
config.enabled = Rails.env.development?
config.show_code_snippets = true
config.code_lines_context = 2
config.trigger_headers = ['X-Plain-Errors', 'X-LLM-Request']
end
# IMPORTANT: Must use insert_before ActionDispatch::ShowExceptions
# Rails includes ShowExceptions by default which catches all exceptions.
Rails.application.config.middleware.insert_before ActionDispatch::ShowExceptions, PlainErrors::Middleware
endWhy insert_before is required:
Rails always includes ActionDispatch::ShowExceptions in the middleware stack, which catches all exceptions and renders error pages. PlainErrors must be inserted before ShowExceptions to intercept exceptions when trigger conditions are met.
Correct middleware order:
PlainErrors::Middleware ← Checks trigger conditions, intercepts if matched
ActionDispatch::ShowExceptions ← Rails default error handler (fallback)
BetterErrors::Middleware ← If installed
PlainErrors should work when used before BetterErrors, Sentry, Honeybadger, and other error handlers:
- When trigger conditions match (e.g.,
X-Plain-Errors: trueheader): PlainErrors returns plain text - When trigger conditions don't match: PlainErrors passes through to standard error handlers (BetterErrors, etc.)
This allows you to use PlainErrors for LLM / automation workflows while keeping BetterErrors for manual debugging.
Important notes:
- Middleware must be configured in an initializer (not in
config/environments/development.rb) - PlainErrors may not catch 404 (route-not-found) errors due to Rails middleware ordering
- Use
config.verbose = truefor debugging if PlainErrors isn't triggering as expected
To use PlainErrors with Claude Code's Playwright MCP server:
- Add or modify Playwright MCP in
~/.claude/claude.json:
{
...
"mcpServers": {
"playwright": {
"type": "stdio",
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--config=~/.claude/playwright-config.json"
],
"env": {}
}
},
...
}This can be a little tricky to hunt down if you have multiple projects servers
configured. Perhaps it could go in a project-specific .claude/claude.json
file? (If you try this, please let me know how it goes!)
- Create
~/.claude/playwright-config.json:
{
"browser": {
"contextOptions": {
"extraHTTPHeaders": {
"X-Plain-Errors": "true"
}
}
}
}This configures Playwright to send the X-Plain-Errors header with all requests, triggering plaintext error output.
PlainErrors decides whether to show plain text errors based on several conditions:
Override all other behavior with query strings:
# Force plaintext errors (overrides Accept headers)
curl http://localhost:3000/endpoint?force_plain_error=1
# Force standard Rails/BetterErrors (overrides all plain error triggers)
curl -H "X-Plain-Errors: 1" http://localhost:3000/endpoint?force_standard_error=1Send X-Plain-Errors with a truthy value (1, true, or yes) or any configured trigger header:
# All of these work:
curl -H "X-Plain-Errors: 1" http://localhost:3000/endpoint
curl -H "X-Plain-Errors: true" http://localhost:3000/endpoint
curl -H "X-Plain-Errors: yes" http://localhost:3000/endpointPlainErrors also checks the Accept header:
- No Accept header → Plain text errors (for CLI tools, API clients)
Accept: text/plain→ Plain text errorsAccept: */*(curl default) → Plain text errorsAccept: text/html→ Standard error handler (BetterErrors, etc.)
# These all trigger plain errors:
curl http://localhost:3000/endpoint # No Accept header
curl -H "Accept: text/plain" http://localhost:3000/endpoint
curl -H "Accept: */*" http://localhost:3000/endpoint
# This uses standard error handler:
curl -H "Accept: text/html" http://localhost:3000/endpointforce_standard_error=1query param (passes through to standard handler)force_plain_error=1query param (shows plain errors)- Configured trigger headers (e.g.,
X-Plain-Errors: 1) - Accept header check (see above)
ERROR
StandardError: This is a test error to verify plain_errors is working!
TRACE
0: app/controllers/debug_controller.rb:5:in `test_error'
1: actionpack (7.2.2.2) lib/action_controller/metal/basic_implicit_render.rb:8:in `send_action'
2: actionpack (7.2.2.2) lib/abstract_controller/base.rb:226:in `process_action'
3: actionpack (7.2.2.2) lib/action_controller/metal/rendering.rb:193:in `process_action'
4: actionpack (7.2.2.2) lib/abstract_controller/callbacks.rb:261:in `block in process_action'
(99 more lines omitted)
app/controllers/debug_controller.rb:5
3: class DebugController < ActionController::Base
4: def test_error
5: raise StandardError, "This is a test error to verify plain_errors is working!"
6: end
7:
8: def middleware
I haven't tested PlainErrors outside of Rails, but it should work in any Rack-based application. If you run into issues with other frameworks, please open an issue.
| Option | Default | Description |
|---|---|---|
enabled |
Rails.env.development? |
Enable/disable the middleware |
show_code_snippets |
true |
Include source code section (set to false to disable entirely) |
code_lines_context |
2 |
Lines of context: 0 = error line only, 1+ = lines before/after |
show_request_info |
false |
Include HTTP request details |
max_stack_trace_lines |
5 |
Max stack trace lines (nil for unlimited) |
application_root |
Rails.root |
Root path for abbreviating paths |
trigger_headers |
['X-Plain-Errors', 'X-LLM-Request'] |
Headers that trigger plaintext output |
verbose |
false |
Enable verbose debug logging to stderr |
show_code_snippets: false→ No code section displayedshow_code_snippets: true+code_lines_context: 0→ Shows only the error lineshow_code_snippets: true+code_lines_context: 2→ Shows 2 lines before and after the error (default)
If PlainErrors isn't working as expected, enable verbose mode to see detailed logging:
# config/initializers/plain_errors.rb
PlainErrors.configure do |config|
# ...
config.verbose = true
endAvailable under the MIT License.