├── Rakefile ├── .gitignore ├── Gemfile ├── lib ├── better_errors │ ├── version.rb │ ├── rails.rb │ ├── repl │ │ ├── basic.rb │ │ └── pry.rb │ ├── repl.rb │ ├── templates │ │ ├── variable_info.erb │ │ └── main.erb │ ├── core_ext │ │ └── exception.rb │ ├── middleware.rb │ ├── code_formatter.rb │ ├── error_page.rb │ └── stack_frame.rb └── better_errors.rb ├── spec ├── spec_helper.rb └── better_errors │ ├── support │ └── my_source.rb │ ├── repl │ └── basic_spec.rb │ ├── middleware_spec.rb │ ├── code_formatter_spec.rb │ ├── error_page_spec.rb │ └── stack_frame_spec.rb ├── LICENSE.txt ├── better_errors.gemspec └── README.md /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | tmp 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/better_errors/version.rb: -------------------------------------------------------------------------------- 1 | module BetterErrors 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path("../../lib", __FILE__) 2 | require "better_errors" 3 | require "ostruct" 4 | -------------------------------------------------------------------------------- /spec/better_errors/support/my_source.rb: -------------------------------------------------------------------------------- 1 | one 2 | two 3 | three 4 | four 5 | five 6 | six 7 | seven 8 | eight 9 | nine 10 | ten 11 | eleven 12 | twelve 13 | thirteen 14 | fourteen 15 | fifteen 16 | sixteen 17 | seventeen 18 | eighteen 19 | nineteen 20 | twenty 21 | -------------------------------------------------------------------------------- /lib/better_errors/rails.rb: -------------------------------------------------------------------------------- 1 | module BetterErrors 2 | class Railtie < Rails::Railtie 3 | initializer "better_errors.configure_rails_initialization" do 4 | unless Rails.env.production? 5 | Rails.application.middleware.use BetterErrors::Middleware 6 | BetterErrors.logger = Rails.logger 7 | BetterErrors.application_root = Rails.root.to_s 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/better_errors/repl/basic.rb: -------------------------------------------------------------------------------- 1 | module BetterErrors 2 | module REPL 3 | class Basic 4 | def initialize(binding) 5 | @binding = binding 6 | end 7 | 8 | def send_input(str) 9 | [execute(str), ">>"] 10 | end 11 | 12 | private 13 | def execute(str) 14 | "=> #{@binding.eval(str).inspect}\n" 15 | rescue Exception => e 16 | "!! #{e.inspect rescue e.class.to_s rescue "Exception"}\n" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/better_errors/repl.rb: -------------------------------------------------------------------------------- 1 | module BetterErrors 2 | module REPL 3 | PROVIDERS = [ 4 | { impl: "better_errors/repl/basic", 5 | const: :Basic }, 6 | ] 7 | 8 | def self.provider 9 | @provider ||= const_get detect[:const] 10 | end 11 | 12 | def self.provider=(prov) 13 | @provider = prov 14 | end 15 | 16 | def self.detect 17 | PROVIDERS.find do |prov| 18 | test_provider prov 19 | end 20 | end 21 | 22 | def self.test_provider(provider) 23 | require provider[:impl] 24 | true 25 | rescue LoadError 26 | false 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/better_errors/templates/variable_info.erb: -------------------------------------------------------------------------------- 1 |
2 |

Local Variables

3 |
4 | 5 | <% @frame.local_variables.each do |name, value| %> 6 | 7 | <% end %> 8 |
<%= name %>
<%= value.inspect %>
9 |
10 |
11 | 12 |
13 |

Instance Variables

14 |
15 | 16 | <% @frame.instance_variables.each do |name, value| %> 17 | 18 | <% end %> 19 |
<%= name %>
<%= value.inspect %>
20 |
21 |
22 | -------------------------------------------------------------------------------- /lib/better_errors/core_ext/exception.rb: -------------------------------------------------------------------------------- 1 | class Exception 2 | original_initialize = instance_method(:initialize) 3 | 4 | if BetterErrors.binding_of_caller_available? 5 | define_method :initialize do |*args| 6 | unless Thread.current[:__better_errors_exception_lock] 7 | Thread.current[:__better_errors_exception_lock] = true 8 | begin 9 | @__better_errors_bindings_stack = binding.callers.drop(1) 10 | ensure 11 | Thread.current[:__better_errors_exception_lock] = false 12 | end 13 | end 14 | original_initialize.bind(self).call(*args) 15 | end 16 | end 17 | 18 | def __better_errors_bindings_stack 19 | @__better_errors_bindings_stack || [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/better_errors/repl/basic_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "better_errors/repl/basic" 3 | 4 | module BetterErrors 5 | module REPL 6 | describe Basic do 7 | let(:fresh_binding) { 8 | local_a = 123 9 | binding 10 | } 11 | 12 | let(:repl) { Basic.new fresh_binding } 13 | 14 | it "should evaluate ruby code in a given context" do 15 | repl.send_input("local_a = 456") 16 | fresh_binding.eval("local_a").should == 456 17 | end 18 | 19 | it "should return a tuple of output and the new prompt" do 20 | output, prompt = repl.send_input("1 + 2") 21 | output.should == "=> 3\n" 22 | prompt.should == ">>" 23 | end 24 | 25 | it "should not barf if the code throws an exception" do 26 | output, prompt = repl.send_input("raise Exception") 27 | output.should == "!! #\n" 28 | prompt.should == ">>" 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/better_errors/middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module BetterErrors 4 | describe Middleware do 5 | it "should pass non-error responses through" do 6 | app = Middleware.new(->env { ":)" }) 7 | app.call({}).should == ":)" 8 | end 9 | 10 | context "when handling an error" do 11 | let(:app) { Middleware.new(->env { raise "oh no :(" }) } 12 | 13 | it "should return status 500" do 14 | status, headers, body = app.call({}) 15 | 16 | status.should == 500 17 | end 18 | 19 | it "should return UTF-8 error pages" do 20 | status, headers, body = app.call({}) 21 | 22 | headers["Content-Type"].should == "text/html; charset=utf-8" 23 | end 24 | 25 | it "should log the exception" do 26 | logger = Object.new 27 | logger.should_receive :fatal 28 | BetterErrors.stub!(:logger).and_return(logger) 29 | 30 | app.call({}) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/better_errors.rb: -------------------------------------------------------------------------------- 1 | require "pp" 2 | require "erubis" 3 | require "coderay" 4 | 5 | require "better_errors/version" 6 | require "better_errors/error_page" 7 | require "better_errors/stack_frame" 8 | require "better_errors/middleware" 9 | require "better_errors/code_formatter" 10 | require "better_errors/repl" 11 | 12 | class << BetterErrors 13 | attr_accessor :application_root, :binding_of_caller_available, :logger 14 | 15 | alias_method :binding_of_caller_available?, :binding_of_caller_available 16 | end 17 | 18 | begin 19 | require "binding_of_caller" 20 | BetterErrors.binding_of_caller_available = true 21 | rescue LoadError => e 22 | BetterErrors.binding_of_caller_available = false 23 | end 24 | 25 | if BetterErrors.binding_of_caller_available? 26 | require "better_errors/core_ext/exception" 27 | else 28 | warn "BetterErrors: binding_of_caller gem unavailable, cannot display local variables on error pages." 29 | warn "Add 'binding_of_caller' to your Gemfile to make this warning go away." 30 | warn "" 31 | end 32 | 33 | require "better_errors/rails" if defined? Rails::Railtie 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Charlie Somerville 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /better_errors.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'better_errors/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "better_errors" 8 | s.version = BetterErrors::VERSION 9 | s.authors = ["Charlie Somerville"] 10 | s.email = ["charlie@charliesomerville.com"] 11 | s.description = %q{Provides a better error page for Rails and other Rack apps. Includes source code inspection, a live REPL and local/instance variable inspection for all stack frames.} 12 | s.summary = %q{Better error page for Rails and other Rack apps} 13 | s.homepage = "https://github.com/charliesome/better_errors" 14 | s.license = "MIT" 15 | 16 | s.files = `git ls-files`.split($/) 17 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 18 | s.require_paths = ["lib"] 19 | 20 | s.add_development_dependency "rake" 21 | 22 | s.add_dependency "erubis", ">= 2.7.0" 23 | s.add_dependency "coderay", ">= 1.0.0" 24 | 25 | # optional dependencies: 26 | # s.add_dependency "binding_of_caller" 27 | # s.add_dependency "pry" 28 | end 29 | -------------------------------------------------------------------------------- /spec/better_errors/code_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module BetterErrors 4 | describe CodeFormatter do 5 | let(:filename) { File.expand_path("../support/my_source.rb", __FILE__) } 6 | 7 | let(:formatter) { CodeFormatter.new(filename, 8) } 8 | 9 | it "should pick an appropriate scanner" do 10 | formatter.coderay_scanner.should == :ruby 11 | end 12 | 13 | it "should show 5 lines of context" do 14 | formatter.line_range.should == (3..13) 15 | 16 | formatter.context_lines.should == [ 17 | "three\n", 18 | "four\n", 19 | "five\n", 20 | "six\n", 21 | "seven\n", 22 | "eight\n", 23 | "nine\n", 24 | "ten\n", 25 | "eleven\n", 26 | "twelve\n", 27 | "thirteen\n" 28 | ] 29 | end 30 | 31 | it "should highlight the erroring line" do 32 | formatter.html.should =~ /highlight.*eight/ 33 | end 34 | 35 | it "should work when the line is right on the edge" do 36 | formatter = CodeFormatter.new(filename, 20) 37 | formatter.line_range.should == (15..20) 38 | formatter.html.should_not == formatter.source_unavailable 39 | end 40 | 41 | it "should not barf when the lines don't make any sense" do 42 | formatter = CodeFormatter.new(filename, 999) 43 | formatter.html.should == formatter.source_unavailable 44 | end 45 | 46 | it "should not barf when the file doesn't exist" do 47 | formatter = CodeFormatter.new("fkdguhskd7e l", 1) 48 | formatter.html.should == formatter.source_unavailable 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/better_errors/middleware.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module BetterErrors 4 | class Middleware 5 | def initialize(app, handler = ErrorPage) 6 | @app = app 7 | @handler = handler 8 | end 9 | 10 | def call(env) 11 | case env["PATH_INFO"] 12 | when %r{\A/__better_errors/(?\d+)/(?\w+)\z} 13 | internal_call env, $~ 14 | when %r{\A/__better_errors/?\z} 15 | show_error_page env 16 | else 17 | app_call env 18 | end 19 | end 20 | 21 | private 22 | def app_call(env) 23 | @app.call env 24 | rescue Exception => ex 25 | @error_page = @handler.new ex, env 26 | log_exception 27 | show_error_page(env) 28 | end 29 | 30 | def show_error_page(env) 31 | [500, { "Content-Type" => "text/html; charset=utf-8" }, [@error_page.render]] 32 | end 33 | 34 | def log_exception 35 | return unless BetterErrors.logger 36 | 37 | message = "\n#{@error_page.exception.class} - #{@error_page.exception.message}:\n" 38 | @error_page.backtrace_frames.each do |frame| 39 | message << " #{frame}\n" 40 | end 41 | 42 | BetterErrors.logger.fatal message 43 | end 44 | 45 | def internal_call(env, opts) 46 | if opts[:oid].to_i != @error_page.object_id 47 | return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]] 48 | end 49 | 50 | response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read)) 51 | [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]] 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/better_errors/code_formatter.rb: -------------------------------------------------------------------------------- 1 | module BetterErrors 2 | class CodeFormatter 3 | FILE_TYPES = { 4 | ".rb" => :ruby, 5 | "" => :ruby, 6 | ".html" => :html, 7 | ".erb" => :erb, 8 | ".haml" => :haml 9 | } 10 | 11 | attr_reader :filename, :line, :context 12 | 13 | def initialize(filename, line, context = 5) 14 | @filename = filename 15 | @line = line 16 | @context = context 17 | end 18 | 19 | def html 20 | %{
#{formatted_lines.join}
} 21 | rescue Errno::ENOENT, Errno::EINVAL 22 | source_unavailable 23 | end 24 | 25 | def source_unavailable 26 | "

Source unavailable

" 27 | end 28 | 29 | def coderay_scanner 30 | ext = File.extname(filename) 31 | FILE_TYPES[ext] || :text 32 | end 33 | 34 | def formatted_lines 35 | line_range.zip(highlighted_lines).map do |current_line, str| 36 | class_name = current_line == line ? "highlight" : "" 37 | sprintf '
%5d %s
', class_name, current_line, str 38 | end 39 | end 40 | 41 | def highlighted_lines 42 | CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines 43 | end 44 | 45 | def context_lines 46 | range = line_range 47 | source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL 48 | end 49 | 50 | def source_lines 51 | @source_lines ||= File.readlines(filename) 52 | end 53 | 54 | def line_range 55 | min = [line - context, 1].max 56 | max = [line + context, source_lines.count].min 57 | min..max 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/better_errors/repl/pry.rb: -------------------------------------------------------------------------------- 1 | require "fiber" 2 | require "pry" 3 | 4 | module BetterErrors 5 | module REPL 6 | class Pry 7 | class Input 8 | def readline 9 | Fiber.yield 10 | end 11 | end 12 | 13 | class Output 14 | def initialize 15 | @buffer = "" 16 | end 17 | 18 | def puts(*args) 19 | args.each do |arg| 20 | @buffer << "#{arg.chomp}\n" 21 | end 22 | end 23 | 24 | def tty? 25 | false 26 | end 27 | 28 | def read_buffer 29 | @buffer 30 | ensure 31 | @buffer = "" 32 | end 33 | end 34 | 35 | def initialize(binding) 36 | @fiber = Fiber.new do 37 | @pry.repl binding 38 | end 39 | @input = Input.new 40 | @output = Output.new 41 | @pry = ::Pry.new input: @input, output: @output 42 | @pry.hooks.clear_all 43 | @continued_expression = false 44 | @pry.hooks.add_hook :after_read, "better_errors hacky hook" do 45 | @continued_expression = false 46 | end 47 | @fiber.resume 48 | end 49 | 50 | def pry_indent 51 | @pry.instance_variable_get(:@indent) 52 | end 53 | 54 | def send_input(str) 55 | old_pry_config_color = ::Pry.config.color 56 | ::Pry.config.color = false 57 | @continued_expression = true 58 | @fiber.resume "#{str}\n" 59 | # TODO - indent with `pry_indent.current_prefix` 60 | # TODO - use proper pry prompt 61 | [@output.read_buffer, @continued_expression ? ".." : ">>"] 62 | ensure 63 | ::Pry.config.color = old_pry_config_color 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/better_errors/error_page_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module BetterErrors 4 | describe ErrorPage do 5 | let(:exception) { raise ZeroDivisionError, "you divided by zero you silly goose!" rescue $! } 6 | 7 | let(:error_page) { ErrorPage.new exception, { "REQUEST_PATH" => "/some/path" } } 8 | 9 | let(:response) { error_page.render } 10 | 11 | let(:empty_binding) { 12 | local_a = :value_for_local_a 13 | local_b = :value_for_local_b 14 | 15 | @inst_c = :value_for_inst_c 16 | @inst_d = :value_for_inst_d 17 | 18 | binding 19 | } 20 | 21 | it "should include the error message" do 22 | response.should include("you divided by zero you silly goose!") 23 | end 24 | 25 | it "should include the request path" do 26 | response.should include("/some/path") 27 | end 28 | 29 | it "should include the exception class" do 30 | response.should include("ZeroDivisionError") 31 | end 32 | 33 | context "variable inspection" do 34 | let(:exception) { empty_binding.eval("raise") rescue $! } 35 | 36 | it "should show local variables" do 37 | html = error_page.do_variables("index" => 0)[:html] 38 | html.should include("local_a") 39 | html.should include(":value_for_local_a") 40 | html.should include("local_b") 41 | html.should include(":value_for_local_b") 42 | end 43 | 44 | it "should show instance variables" do 45 | html = error_page.do_variables("index" => 0)[:html] 46 | html.should include("inst_c") 47 | html.should include(":value_for_inst_c") 48 | html.should include("inst_d") 49 | html.should include(":value_for_inst_d") 50 | end 51 | end 52 | 53 | it "should not die if the source file is not a real filename" do 54 | exception.stub!(:backtrace).and_return([ 55 | ":10:in `spawn_rack_application'" 56 | ]) 57 | response.should include("Source unavailable") 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/better_errors/error_page.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module BetterErrors 4 | class ErrorPage 5 | def self.template_path(template_name) 6 | File.expand_path("../templates/#{template_name}.erb", __FILE__) 7 | end 8 | 9 | def self.template(template_name) 10 | Erubis::EscapedEruby.new(File.read(template_path(template_name))) 11 | end 12 | 13 | attr_reader :exception, :env, :repls 14 | 15 | def initialize(exception, env) 16 | @exception = real_exception(exception) 17 | @env = env 18 | @start_time = Time.now.to_f 19 | @repls = [] 20 | end 21 | 22 | def render(template_name = "main") 23 | self.class.template(template_name).result binding 24 | end 25 | 26 | def do_variables(opts) 27 | index = opts["index"].to_i 28 | @frame = backtrace_frames[index] 29 | { html: render("variable_info") } 30 | end 31 | 32 | def do_eval(opts) 33 | index = opts["index"].to_i 34 | code = opts["source"] 35 | 36 | unless binding = backtrace_frames[index].frame_binding 37 | return { error: "REPL unavailable in this stack frame" } 38 | end 39 | 40 | result, prompt = 41 | (@repls[index] ||= REPL.provider.new(binding)).send_input(code) 42 | 43 | { result: result, 44 | prompt: prompt, 45 | highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) } 46 | end 47 | 48 | def backtrace_frames 49 | @backtrace_frames ||= StackFrame.from_exception(exception) 50 | end 51 | 52 | private 53 | def exception_message 54 | if exception.is_a?(SyntaxError) && exception.message =~ /\A.*:\d*: (.*)$/ 55 | $1 56 | else 57 | exception.message 58 | end 59 | end 60 | 61 | def real_exception(exception) 62 | if exception.respond_to? :original_exception 63 | exception.original_exception 64 | else 65 | exception 66 | end 67 | end 68 | 69 | def request_path 70 | env["REQUEST_PATH"] 71 | end 72 | 73 | def highlighted_code_block(frame) 74 | CodeFormatter.new(frame.filename, frame.line).html 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Errors 2 | 3 | Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails in any Rack app as Rack middleware. 4 | 5 | ![image](http://i.imgur.com/Me5of.png) 6 | 7 | ## Features 8 | 9 | * Full stack trace 10 | * Source code inspection for all stack frames (with highlighting) 11 | * Local and instance variable inspection 12 | * Live REPL on every stack frame 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile (under the **development** group): 17 | 18 | ```ruby 19 | group :development do 20 | gem "better_errors" 21 | end 22 | ``` 23 | 24 | If you would like to use Better Errors' **advanced features** (REPL, local/instance variable inspection, pretty stack frame names), you need to add the [`binding_of_caller`](https://github.com/banister/binding_of_caller) gem to your Gemfile: 25 | 26 | ```ruby 27 | gem "binding_of_caller" 28 | ``` 29 | 30 | This is an optional dependency however, and Better Errors will work without it. 31 | 32 | ## Usage 33 | 34 | If you're using Rails, there's nothing else you need to do. 35 | 36 | If you're not using Rails, you need to insert `BetterErrors::Middleware` into your middleware stack, and optionally set `BetterErrors.application_root` if you'd like Better Errors to abbreviate filenames within your application. 37 | 38 | Here's an example using Sinatra: 39 | 40 | ```ruby 41 | require "sinatra" 42 | require "better_errors" 43 | 44 | use BetterErrors::Middleware 45 | BetterErrors.application_root = File.expand_path("..", __FILE__) 46 | 47 | get "/" do 48 | raise "oops" 49 | end 50 | ``` 51 | 52 | ## Compatibility 53 | 54 | * **Supported** 55 | * MRI 1.9.2, 1.9.3 56 | * JRuby (1.9 mode) - *advanced features unsupported* 57 | * Rubinius (1.9 mode) - *advanced features unsupported* 58 | * **Coming soon** 59 | * MRI 2.0.0 - the official API for grabbing caller bindings is slated for MRI 2.0.0, but it has not been implemented yet 60 | 61 | ## Known issues 62 | 63 | * Calling `yield` from the REPL segfaults MRI 1.9.x. 64 | 65 | ## Contributing 66 | 67 | 1. Fork it 68 | 2. Create your feature branch (`git checkout -b my-new-feature`) 69 | 3. Commit your changes (`git commit -am 'Add some feature'`) 70 | 4. Push to the branch (`git push origin my-new-feature`) 71 | 5. Create new Pull Request 72 | -------------------------------------------------------------------------------- /spec/better_errors/stack_frame_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module BetterErrors 4 | describe StackFrame do 5 | context "#application?" do 6 | it "should be true for application filenames" do 7 | BetterErrors.stub!(:application_root).and_return("/abc/xyz") 8 | frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") 9 | 10 | frame.application?.should be_true 11 | end 12 | 13 | it "should be false for everything else" do 14 | BetterErrors.stub!(:application_root).and_return("/abc/xyz") 15 | frame = StackFrame.new("/abc/nope", 123, "foo") 16 | 17 | frame.application?.should be_false 18 | end 19 | 20 | it "should not care if no application_root is set" do 21 | frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") 22 | 23 | frame.application?.should be_false 24 | end 25 | end 26 | 27 | context "#gem?" do 28 | it "should be true for gem filenames" do 29 | Gem.stub!(:path).and_return(["/abc/xyz"]) 30 | frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") 31 | 32 | frame.gem?.should be_true 33 | end 34 | 35 | it "should be false for everything else" do 36 | Gem.stub!(:path).and_return(["/abc/xyz"]) 37 | frame = StackFrame.new("/abc/nope", 123, "foo") 38 | 39 | frame.gem?.should be_false 40 | end 41 | end 42 | 43 | context "#application_path" do 44 | it "should chop off the application root" do 45 | BetterErrors.stub!(:application_root).and_return("/abc/xyz") 46 | frame = StackFrame.new("/abc/xyz/app/controllers/crap_controller.rb", 123, "index") 47 | 48 | frame.application_path.should == "app/controllers/crap_controller.rb" 49 | end 50 | end 51 | 52 | context "#gem_path" do 53 | it "should chop of the gem path and stick (gem) there" do 54 | Gem.stub!(:path).and_return(["/abc/xyz"]) 55 | frame = StackFrame.new("/abc/xyz/gems/whatever-1.2.3/lib/whatever.rb", 123, "foo") 56 | 57 | frame.gem_path.should == "(gem) whatever-1.2.3/lib/whatever.rb" 58 | end 59 | end 60 | 61 | it "should special case SyntaxErrors" do 62 | syntax_error = SyntaxError.new "my_file.rb:123: you wrote bad ruby!" 63 | syntax_error.stub!(:backtrace).and_return([]) 64 | frames = StackFrame.from_exception(syntax_error) 65 | frames.count.should == 1 66 | frames.first.filename.should == "my_file.rb" 67 | frames.first.line.should == 123 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/better_errors/stack_frame.rb: -------------------------------------------------------------------------------- 1 | module BetterErrors 2 | class StackFrame 3 | def self.from_exception(exception) 4 | idx_offset = 0 5 | list = exception.backtrace.each_with_index.map do |frame, idx| 6 | frame_binding = exception.__better_errors_bindings_stack[idx - idx_offset] 7 | md = /\A(?.*):(?\d*):in `(?.*)'\z/.match(frame) 8 | 9 | # prevent mismatching frames in the backtrace with the binding stack 10 | if frame_binding and frame_binding.eval("__FILE__") != md[:file] 11 | idx_offset += 1 12 | frame_binding = nil 13 | end 14 | 15 | StackFrame.new(md[:file], md[:line].to_i, md[:name], frame_binding) 16 | end 17 | 18 | if exception.is_a?(SyntaxError) && exception.to_s =~ /\A(.*):(\d*):/ 19 | list.unshift StackFrame.new($1, $2.to_i, "") 20 | end 21 | 22 | list 23 | end 24 | 25 | attr_reader :filename, :line, :name, :frame_binding 26 | 27 | def initialize(filename, line, name, frame_binding = nil) 28 | @filename = filename 29 | @line = line 30 | @name = name 31 | @frame_binding = frame_binding 32 | 33 | set_pretty_method_name if frame_binding 34 | end 35 | 36 | def application? 37 | root = BetterErrors.application_root 38 | starts_with? filename, root if root 39 | end 40 | 41 | def application_path 42 | filename[(BetterErrors.application_root.length+1)..-1] 43 | end 44 | 45 | def gem? 46 | Gem.path.any? { |path| starts_with? filename, path } 47 | end 48 | 49 | def gem_path 50 | Gem.path.each do |path| 51 | if starts_with? filename, path 52 | return filename.gsub("#{path}/gems/", "(gem) ") 53 | end 54 | end 55 | end 56 | 57 | def name_parts 58 | @name_parts ||= name.match(/^(.*?)([\.\#].*)$/) 59 | end 60 | 61 | def class_name 62 | name_parts && name_parts[1] 63 | end 64 | 65 | def method_name 66 | name_parts && name_parts[2] 67 | end 68 | 69 | def context 70 | if application? 71 | :application 72 | elsif gem? 73 | :gem 74 | else 75 | :dunno 76 | end 77 | end 78 | 79 | def pretty_path 80 | case context 81 | when :application; application_path 82 | when :gem; gem_path 83 | else filename 84 | end 85 | end 86 | 87 | def local_variables 88 | return {} unless frame_binding 89 | frame_binding.eval("local_variables").each_with_object({}) do |name, hash| 90 | begin 91 | hash[name] = frame_binding.eval(name.to_s) 92 | rescue NameError => e 93 | # local_variables sometimes returns broken variables. 94 | # https://bugs.ruby-lang.org/issues/7536 95 | end 96 | end 97 | end 98 | 99 | def instance_variables 100 | return {} unless frame_binding 101 | Hash[frame_binding.eval("instance_variables").map { |x| 102 | [x, frame_binding.eval(x.to_s)] 103 | }] 104 | end 105 | 106 | def to_s 107 | "#{pretty_path}:#{line}:in `#{name}'" 108 | end 109 | 110 | private 111 | def set_pretty_method_name 112 | name =~ /\A(block (\([^)]+\) )?in )?/ 113 | recv = frame_binding.eval("self") 114 | return unless method = frame_binding.eval("__method__") 115 | @name = if recv.is_a? Module 116 | "#{$1}#{recv}.#{method}" 117 | else 118 | "#{$1}#{recv.class}##{method}" 119 | end 120 | end 121 | 122 | def starts_with?(haystack, needle) 123 | haystack[0, needle.length] == needle 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/better_errors/templates/main.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= exception.class %> at <%= request_path %> 5 | 647 | 648 | 649 |
650 |
651 |

<%= exception.class %> at <%= request_path %>

652 |

<%= exception_message %>

653 |
654 | 655 |
656 | 657 |
658 | 685 | 686 | <% backtrace_frames.each_with_index do |frame, index| %> 687 | 718 | <% end %> 719 |
720 |
721 | 722 | 937 | 938 | 939 | --------------------------------------------------------------------------------