better_errors-creditkudos 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +8 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +128 -0
- data/Rakefile +13 -0
- data/better_errors-creditkudos.gemspec +28 -0
- data/lib/better_errors.rb +152 -0
- data/lib/better_errors/code_formatter.rb +63 -0
- data/lib/better_errors/code_formatter/html.rb +26 -0
- data/lib/better_errors/code_formatter/text.rb +14 -0
- data/lib/better_errors/error_page.rb +129 -0
- data/lib/better_errors/exception_extension.rb +17 -0
- data/lib/better_errors/middleware.rb +141 -0
- data/lib/better_errors/rails.rb +28 -0
- data/lib/better_errors/raised_exception.rb +68 -0
- data/lib/better_errors/repl.rb +30 -0
- data/lib/better_errors/repl/basic.rb +20 -0
- data/lib/better_errors/repl/pry.rb +78 -0
- data/lib/better_errors/stack_frame.rb +111 -0
- data/lib/better_errors/templates/main.erb +1032 -0
- data/lib/better_errors/templates/text.erb +21 -0
- data/lib/better_errors/templates/variable_info.erb +72 -0
- data/lib/better_errors/version.rb +3 -0
- data/spec/better_errors/code_formatter_spec.rb +92 -0
- data/spec/better_errors/error_page_spec.rb +122 -0
- data/spec/better_errors/middleware_spec.rb +180 -0
- data/spec/better_errors/raised_exception_spec.rb +72 -0
- data/spec/better_errors/repl/basic_spec.rb +18 -0
- data/spec/better_errors/repl/pry_spec.rb +40 -0
- data/spec/better_errors/repl/shared_examples.rb +18 -0
- data/spec/better_errors/stack_frame_spec.rb +157 -0
- data/spec/better_errors/support/my_source.rb +20 -0
- data/spec/better_errors_spec.rb +73 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/without_binding_of_caller.rb +9 -0
- metadata +136 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
<%== text_heading("=", "%s at %s" % [exception_type, request_path]) %>
|
2
|
+
|
3
|
+
> <%== exception_message %>
|
4
|
+
<% if backtrace_frames.any? %>
|
5
|
+
|
6
|
+
<%== text_heading("-", "%s, line %i" % [first_frame.pretty_path, first_frame.line]) %>
|
7
|
+
|
8
|
+
``` ruby
|
9
|
+
<%== text_formatted_code_block(first_frame) %>```
|
10
|
+
|
11
|
+
App backtrace
|
12
|
+
-------------
|
13
|
+
|
14
|
+
<%== application_frames.map { |s| " - #{s}" }.join("\n") %>
|
15
|
+
|
16
|
+
Full backtrace
|
17
|
+
--------------
|
18
|
+
|
19
|
+
<%== backtrace_frames.map { |s| " - #{s}" }.join("\n") %>
|
20
|
+
|
21
|
+
<% end %>
|
@@ -0,0 +1,72 @@
|
|
1
|
+
<header class="trace_info clearfix">
|
2
|
+
<div class="title">
|
3
|
+
<h2 class="name"><%= @frame.name %></h2>
|
4
|
+
<div class="location"><span class="filename"><a href="<%= editor_url(@frame) %>"><%= @frame.pretty_path %></a></span></div>
|
5
|
+
</div>
|
6
|
+
<div class="code_block clearfix">
|
7
|
+
<%== html_formatted_code_block @frame %>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<% if BetterErrors.binding_of_caller_available? && @frame.frame_binding %>
|
11
|
+
<div class="repl">
|
12
|
+
<div class="console">
|
13
|
+
<pre></pre>
|
14
|
+
<div class="prompt"><span>>></span> <input/></div>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
18
|
+
</header>
|
19
|
+
|
20
|
+
<% if BetterErrors.binding_of_caller_available? && @frame.frame_binding %>
|
21
|
+
<div class="hint">
|
22
|
+
This is a live shell. Type in here.
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<div class="variable_info"></div>
|
26
|
+
<% end %>
|
27
|
+
|
28
|
+
<% unless BetterErrors.binding_of_caller_available? %>
|
29
|
+
<div class="hint">
|
30
|
+
<strong>Tip:</strong> add <code>gem "binding_of_caller"</code> to your Gemfile to enable the REPL and local/instance variable inspection.
|
31
|
+
</div>
|
32
|
+
<% end %>
|
33
|
+
|
34
|
+
<div class="sub">
|
35
|
+
<h3>Request info</h3>
|
36
|
+
<div class='inset variables'>
|
37
|
+
<table class="var_table">
|
38
|
+
<% if rails_params %>
|
39
|
+
<tr><td class="name">Request parameters</td><td><pre><%== inspect_value rails_params %></pre></td></tr>
|
40
|
+
<% end %>
|
41
|
+
<% if rack_session %>
|
42
|
+
<tr><td class="name">Rack session</td><td><pre><%== inspect_value rack_session %></pre></td></tr>
|
43
|
+
<% end %>
|
44
|
+
</table>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<% if BetterErrors.binding_of_caller_available? && @frame.frame_binding %>
|
49
|
+
<div class="sub">
|
50
|
+
<h3>Local Variables</h3>
|
51
|
+
<div class='inset variables'>
|
52
|
+
<table class="var_table">
|
53
|
+
<% @frame.local_variables.each do |name, value| %>
|
54
|
+
<tr><td class="name"><%= name %></td><td><pre><%== inspect_value value %></pre></td></tr>
|
55
|
+
<% end %>
|
56
|
+
</table>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
|
60
|
+
<div class="sub">
|
61
|
+
<h3>Instance Variables</h3>
|
62
|
+
<div class="inset variables">
|
63
|
+
<table class="var_table">
|
64
|
+
<% @frame.instance_variables.each do |name, value| %>
|
65
|
+
<tr><td class="name"><%= name %></td><td><pre><%== inspect_value value %></pre></td></tr>
|
66
|
+
<% end %>
|
67
|
+
</table>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
<!-- <%= Time.now.to_f - @var_start_time %> seconds -->
|
72
|
+
<% end %>
|
@@ -0,0 +1,92 @@
|
|
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 "picks an appropriate scanner" do
|
10
|
+
formatter.coderay_scanner.should == :ruby
|
11
|
+
end
|
12
|
+
|
13
|
+
it "shows 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 "works when the line is right on the edge" do
|
32
|
+
formatter = CodeFormatter.new(filename, 20)
|
33
|
+
formatter.line_range.should == (15..20)
|
34
|
+
end
|
35
|
+
|
36
|
+
describe CodeFormatter::HTML do
|
37
|
+
it "highlights the erroring line" do
|
38
|
+
formatter = CodeFormatter::HTML.new(filename, 8)
|
39
|
+
formatter.output.should =~ /highlight.*eight/
|
40
|
+
end
|
41
|
+
|
42
|
+
it "works when the line is right on the edge" do
|
43
|
+
formatter = CodeFormatter::HTML.new(filename, 20)
|
44
|
+
formatter.output.should_not == formatter.source_unavailable
|
45
|
+
end
|
46
|
+
|
47
|
+
it "doesn't barf when the lines don't make any sense" do
|
48
|
+
formatter = CodeFormatter::HTML.new(filename, 999)
|
49
|
+
formatter.output.should == formatter.source_unavailable
|
50
|
+
end
|
51
|
+
|
52
|
+
it "doesn't barf when the file doesn't exist" do
|
53
|
+
formatter = CodeFormatter::HTML.new("fkdguhskd7e l", 1)
|
54
|
+
formatter.output.should == formatter.source_unavailable
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe CodeFormatter::Text do
|
59
|
+
it "highlights the erroring line" do
|
60
|
+
formatter = CodeFormatter::Text.new(filename, 8)
|
61
|
+
formatter.output.should == <<-TEXT.gsub(/^ /, "")
|
62
|
+
3 three
|
63
|
+
4 four
|
64
|
+
5 five
|
65
|
+
6 six
|
66
|
+
7 seven
|
67
|
+
> 8 eight
|
68
|
+
9 nine
|
69
|
+
10 ten
|
70
|
+
11 eleven
|
71
|
+
12 twelve
|
72
|
+
13 thirteen
|
73
|
+
TEXT
|
74
|
+
end
|
75
|
+
|
76
|
+
it "works when the line is right on the edge" do
|
77
|
+
formatter = CodeFormatter::Text.new(filename, 20)
|
78
|
+
formatter.output.should_not == formatter.source_unavailable
|
79
|
+
end
|
80
|
+
|
81
|
+
it "doesn't barf when the lines don't make any sense" do
|
82
|
+
formatter = CodeFormatter::Text.new(filename, 999)
|
83
|
+
formatter.output.should == formatter.source_unavailable
|
84
|
+
end
|
85
|
+
|
86
|
+
it "doesn't barf when the file doesn't exist" do
|
87
|
+
formatter = CodeFormatter::Text.new("fkdguhskd7e l", 1)
|
88
|
+
formatter.output.should == formatter.source_unavailable
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,122 @@
|
|
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, { "PATH_INFO" => "/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 "includes the error message" do
|
22
|
+
response.should include("you divided by zero you silly goose!")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "includes the request path" do
|
26
|
+
response.should include("/some/path")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "includes 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
|
+
if BetterErrors.binding_of_caller_available?
|
37
|
+
it "shows local variables" do
|
38
|
+
html = error_page.do_variables("index" => 0)[:html]
|
39
|
+
html.should include("local_a")
|
40
|
+
html.should include(":value_for_local_a")
|
41
|
+
html.should include("local_b")
|
42
|
+
html.should include(":value_for_local_b")
|
43
|
+
end
|
44
|
+
else
|
45
|
+
it "tells the user to add binding_of_caller to their gemfile to get fancy features" do
|
46
|
+
html = error_page.do_variables("index" => 0)[:html]
|
47
|
+
html.should include(%{gem "binding_of_caller"})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "shows instance variables" do
|
52
|
+
html = error_page.do_variables("index" => 0)[:html]
|
53
|
+
html.should include("inst_c")
|
54
|
+
html.should include(":value_for_inst_c")
|
55
|
+
html.should include("inst_d")
|
56
|
+
html.should include(":value_for_inst_d")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "shows filter instance variables" do
|
60
|
+
BetterErrors.stub(:ignored_instance_variables).and_return([ :@inst_d ])
|
61
|
+
html = error_page.do_variables("index" => 0)[:html]
|
62
|
+
html.should include("inst_c")
|
63
|
+
html.should include(":value_for_inst_c")
|
64
|
+
html.should_not include('<td class="name">@inst_d</td>')
|
65
|
+
html.should_not include("<pre>:value_for_inst_d</pre>")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it "doesn't die if the source file is not a real filename" do
|
70
|
+
exception.stub(:backtrace).and_return([
|
71
|
+
"<internal:prelude>:10:in `spawn_rack_application'"
|
72
|
+
])
|
73
|
+
response.should include("Source unavailable")
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with an exception with blank lines' do
|
77
|
+
class SpacedError < StandardError
|
78
|
+
def initialize(message = nil)
|
79
|
+
message = "\n\n#{message}" if message
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
let!(:exception) { raise SpacedError, "Danger Warning!" rescue $! }
|
85
|
+
|
86
|
+
it 'should not include leading blank lines from exception_message' do
|
87
|
+
exception.message.should =~ /\A\n\n/
|
88
|
+
error_page.exception_message.should_not =~ /\A\n\n/
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'with an inspect size limit set' do
|
93
|
+
before { BetterErrors.maximum_variable_inspect_size = 50_000 }
|
94
|
+
|
95
|
+
it "shows variables with inspects that are below the inspect size threshold" do
|
96
|
+
content = 'AAAAA'
|
97
|
+
empty_binding.instance_variable_set('@small', content)
|
98
|
+
|
99
|
+
html = error_page.do_variables("index" => 0)[:html]
|
100
|
+
html.should_not include "object too large"
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
it "hides variables with inspects that are above the inspect size threshold" do
|
105
|
+
content = 'A' * (BetterErrors.maximum_variable_inspect_size)
|
106
|
+
empty_binding.instance_variable_set('@big', content)
|
107
|
+
|
108
|
+
html = error_page.do_variables("index" => 0)[:html]
|
109
|
+
html.should include "object too large"
|
110
|
+
end
|
111
|
+
|
112
|
+
it "shows variables with large inspects if max inspect size is disabled" do
|
113
|
+
content = 'A' * (BetterErrors.maximum_variable_inspect_size)
|
114
|
+
BetterErrors.maximum_variable_inspect_size = nil
|
115
|
+
empty_binding.instance_variable_set('@big', content)
|
116
|
+
|
117
|
+
html = error_page.do_variables("index" => 0)[:html]
|
118
|
+
html.should_not include "object too large"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module BetterErrors
|
4
|
+
describe Middleware do
|
5
|
+
let(:app) { Middleware.new(->env { ":)" }) }
|
6
|
+
let(:exception) { RuntimeError.new("oh no :(") }
|
7
|
+
|
8
|
+
it "passes non-error responses through" do
|
9
|
+
app.call({}).should == ":)"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "calls the internal methods" do
|
13
|
+
app.should_receive :internal_call
|
14
|
+
app.call("PATH_INFO" => "/__better_errors/1/preform_awesomness")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "calls the internal methods on any subfolder path" do
|
18
|
+
app.should_receive :internal_call
|
19
|
+
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/1/preform_awesomness")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "shows the error page" do
|
23
|
+
app.should_receive :show_error_page
|
24
|
+
app.call("PATH_INFO" => "/__better_errors/")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "shows the error page on any subfolder path" do
|
28
|
+
app.should_receive :show_error_page
|
29
|
+
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/")
|
30
|
+
end
|
31
|
+
|
32
|
+
it "doesn't show the error page to a non-local address" do
|
33
|
+
app.should_not_receive :better_errors_call
|
34
|
+
app.call("REMOTE_ADDR" => "1.2.3.4")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "shows to a whitelisted IP" do
|
38
|
+
BetterErrors::Middleware.allow_ip! '77.55.33.11'
|
39
|
+
app.should_receive :better_errors_call
|
40
|
+
app.call("REMOTE_ADDR" => "77.55.33.11")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "respects the X-Forwarded-For header" do
|
44
|
+
app.should_not_receive :better_errors_call
|
45
|
+
app.call(
|
46
|
+
"REMOTE_ADDR" => "127.0.0.1",
|
47
|
+
"HTTP_X_FORWARDED_FOR" => "1.2.3.4",
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "doesn't blow up when given a blank REMOTE_ADDR" do
|
52
|
+
expect { app.call("REMOTE_ADDR" => " ") }.to_not raise_error
|
53
|
+
end
|
54
|
+
|
55
|
+
it "doesn't blow up when given an IP address with a zone index" do
|
56
|
+
expect { app.call("REMOTE_ADDR" => "0:0:0:0:0:0:0:1%0" ) }.to_not raise_error
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when requesting the /__better_errors manually" do
|
60
|
+
let(:app) { Middleware.new(->env { ":)" }) }
|
61
|
+
|
62
|
+
it "shows that no errors have been recorded" do
|
63
|
+
status, headers, body = app.call("PATH_INFO" => "/__better_errors")
|
64
|
+
body.join.should match /No errors have been recorded yet./
|
65
|
+
end
|
66
|
+
|
67
|
+
it "shows that no errors have been recorded on any subfolder path" do
|
68
|
+
status, headers, body = app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors")
|
69
|
+
body.join.should match /No errors have been recorded yet./
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when handling an error" do
|
74
|
+
let(:app) { Middleware.new(->env { raise exception }) }
|
75
|
+
|
76
|
+
it "returns status 500" do
|
77
|
+
status, headers, body = app.call({})
|
78
|
+
|
79
|
+
status.should == 500
|
80
|
+
end
|
81
|
+
|
82
|
+
if Exception.new.respond_to?(:cause)
|
83
|
+
context "cause" do
|
84
|
+
class OtherException < Exception
|
85
|
+
def initialize(message)
|
86
|
+
super(message)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it "shows Original Exception if it responds_to and has an cause" do
|
91
|
+
app = Middleware.new(->env {
|
92
|
+
begin
|
93
|
+
raise "Original Exception"
|
94
|
+
rescue
|
95
|
+
raise OtherException.new("Other Exception")
|
96
|
+
end
|
97
|
+
})
|
98
|
+
|
99
|
+
status, _, body = app.call({})
|
100
|
+
|
101
|
+
status.should == 500
|
102
|
+
body.join.should_not match(/\n> Other Exception\n/)
|
103
|
+
body.join.should match(/\n> Original Exception\n/)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
else
|
107
|
+
context "original_exception" do
|
108
|
+
class OriginalExceptionException < Exception
|
109
|
+
attr_reader :original_exception
|
110
|
+
|
111
|
+
def initialize(message, original_exception = nil)
|
112
|
+
super(message)
|
113
|
+
@original_exception = original_exception
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it "shows Original Exception if it responds_to and has an original_exception" do
|
118
|
+
app = Middleware.new(->env {
|
119
|
+
raise OriginalExceptionException.new("Other Exception", Exception.new("Original Exception"))
|
120
|
+
})
|
121
|
+
|
122
|
+
status, _, body = app.call({})
|
123
|
+
|
124
|
+
status.should == 500
|
125
|
+
body.join.should_not match(/Other Exception/)
|
126
|
+
body.join.should match(/Original Exception/)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "won't crash if the exception responds_to but doesn't have an original_exception" do
|
130
|
+
app = Middleware.new(->env {
|
131
|
+
raise OriginalExceptionException.new("Other Exception")
|
132
|
+
})
|
133
|
+
|
134
|
+
status, _, body = app.call({})
|
135
|
+
|
136
|
+
status.should == 500
|
137
|
+
body.join.should match(/Other Exception/)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
it "returns ExceptionWrapper's status_code" do
|
143
|
+
ad_ew = double("ActionDispatch::ExceptionWrapper")
|
144
|
+
ad_ew.stub('new').with({}, exception ){ double("ExceptionWrapper", status_code: 404) }
|
145
|
+
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
|
146
|
+
|
147
|
+
status, headers, body = app.call({})
|
148
|
+
|
149
|
+
status.should == 404
|
150
|
+
end
|
151
|
+
|
152
|
+
it "returns UTF-8 error pages" do
|
153
|
+
status, headers, body = app.call({})
|
154
|
+
|
155
|
+
headers["Content-Type"].should match /charset=utf-8/
|
156
|
+
end
|
157
|
+
|
158
|
+
it "returns text pages by default" do
|
159
|
+
status, headers, body = app.call({})
|
160
|
+
|
161
|
+
headers["Content-Type"].should match /text\/plain/
|
162
|
+
end
|
163
|
+
|
164
|
+
it "returns HTML pages by default" do
|
165
|
+
# Chrome's 'Accept' header looks similar this.
|
166
|
+
status, headers, body = app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*")
|
167
|
+
|
168
|
+
headers["Content-Type"].should match /text\/html/
|
169
|
+
end
|
170
|
+
|
171
|
+
it "logs the exception" do
|
172
|
+
logger = Object.new
|
173
|
+
logger.should_receive :fatal
|
174
|
+
BetterErrors.stub(:logger).and_return(logger)
|
175
|
+
|
176
|
+
app.call({})
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|