better_errors-creditkudos 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,26 @@
|
|
1
|
+
module BetterErrors
|
2
|
+
# @private
|
3
|
+
class CodeFormatter::HTML < CodeFormatter
|
4
|
+
def source_unavailable
|
5
|
+
"<p class='unavailable'>Source is not available</p>"
|
6
|
+
end
|
7
|
+
|
8
|
+
def formatted_lines
|
9
|
+
each_line_of(highlighted_lines) { |highlight, current_line, str|
|
10
|
+
class_name = highlight ? "highlight" : ""
|
11
|
+
sprintf '<pre class="%s">%s</pre>', class_name, str
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def formatted_nums
|
16
|
+
each_line_of(highlighted_lines) { |highlight, current_line, str|
|
17
|
+
class_name = highlight ? "highlight" : ""
|
18
|
+
sprintf '<span class="%s">%5d</span>', class_name, current_line
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def formatted_code
|
23
|
+
%{<div class="code_linenums">#{formatted_nums.join}</div><div class="code">#{super}</div>}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module BetterErrors
|
2
|
+
# @private
|
3
|
+
class CodeFormatter::Text < CodeFormatter
|
4
|
+
def source_unavailable
|
5
|
+
"# Source is not available"
|
6
|
+
end
|
7
|
+
|
8
|
+
def formatted_lines
|
9
|
+
each_line_of(context_lines) { |highlight, current_line, str|
|
10
|
+
sprintf '%s %3d %s', (highlight ? '>' : ' '), current_line, str
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "json"
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module BetterErrors
|
6
|
+
# @private
|
7
|
+
class ErrorPage
|
8
|
+
def self.template_path(template_name)
|
9
|
+
File.expand_path("../templates/#{template_name}.erb", __FILE__)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.template(template_name)
|
13
|
+
Erubis::EscapedEruby.new(File.read(template_path(template_name)))
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :exception, :env, :repls
|
17
|
+
|
18
|
+
def initialize(exception, env)
|
19
|
+
@exception = RaisedException.new(exception)
|
20
|
+
@env = env
|
21
|
+
@start_time = Time.now.to_f
|
22
|
+
@repls = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def id
|
26
|
+
@id ||= SecureRandom.hex(8)
|
27
|
+
end
|
28
|
+
|
29
|
+
def render(template_name = "main")
|
30
|
+
self.class.template(template_name).result binding
|
31
|
+
end
|
32
|
+
|
33
|
+
def do_variables(opts)
|
34
|
+
index = opts["index"].to_i
|
35
|
+
@frame = backtrace_frames[index]
|
36
|
+
@var_start_time = Time.now.to_f
|
37
|
+
{ html: render("variable_info") }
|
38
|
+
end
|
39
|
+
|
40
|
+
def do_eval(opts)
|
41
|
+
index = opts["index"].to_i
|
42
|
+
code = opts["source"]
|
43
|
+
|
44
|
+
unless binding = backtrace_frames[index].frame_binding
|
45
|
+
return { error: "REPL unavailable in this stack frame" }
|
46
|
+
end
|
47
|
+
|
48
|
+
result, prompt, prefilled_input =
|
49
|
+
(@repls[index] ||= REPL.provider.new(binding)).send_input(code)
|
50
|
+
|
51
|
+
{ result: result,
|
52
|
+
prompt: prompt,
|
53
|
+
prefilled_input: prefilled_input,
|
54
|
+
highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def backtrace_frames
|
58
|
+
exception.backtrace
|
59
|
+
end
|
60
|
+
|
61
|
+
def exception_type
|
62
|
+
exception.type
|
63
|
+
end
|
64
|
+
|
65
|
+
def exception_message
|
66
|
+
exception.message.lstrip
|
67
|
+
end
|
68
|
+
|
69
|
+
def application_frames
|
70
|
+
backtrace_frames.select(&:application?)
|
71
|
+
end
|
72
|
+
|
73
|
+
def first_frame
|
74
|
+
application_frames.first || backtrace_frames.first
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def editor_url(frame)
|
79
|
+
BetterErrors.editor[frame.filename, frame.line]
|
80
|
+
end
|
81
|
+
|
82
|
+
def rack_session
|
83
|
+
env['rack.session']
|
84
|
+
end
|
85
|
+
|
86
|
+
def rails_params
|
87
|
+
env['action_dispatch.request.parameters']
|
88
|
+
end
|
89
|
+
|
90
|
+
def uri_prefix
|
91
|
+
env["SCRIPT_NAME"] || ""
|
92
|
+
end
|
93
|
+
|
94
|
+
def request_path
|
95
|
+
env["PATH_INFO"]
|
96
|
+
end
|
97
|
+
|
98
|
+
def html_formatted_code_block(frame)
|
99
|
+
CodeFormatter::HTML.new(frame.filename, frame.line).output
|
100
|
+
end
|
101
|
+
|
102
|
+
def text_formatted_code_block(frame)
|
103
|
+
CodeFormatter::Text.new(frame.filename, frame.line).output
|
104
|
+
end
|
105
|
+
|
106
|
+
def text_heading(char, str)
|
107
|
+
str + "\n" + char*str.size
|
108
|
+
end
|
109
|
+
|
110
|
+
def inspect_value(obj)
|
111
|
+
inspect_raw_value(obj)
|
112
|
+
rescue NoMethodError
|
113
|
+
"<span class='unsupported'>(object doesn't support inspect)</span>"
|
114
|
+
rescue Exception => e
|
115
|
+
binding.pry
|
116
|
+
"<span class='unsupported'>(exception #{CGI.escapeHTML(e.class.to_s)} was raised in inspect)</span>"
|
117
|
+
end
|
118
|
+
|
119
|
+
def inspect_raw_value(obj)
|
120
|
+
value = CGI.escapeHTML(obj.inspect)
|
121
|
+
|
122
|
+
if !BetterErrors.maximum_variable_inspect_size.nil? && value.length > BetterErrors.maximum_variable_inspect_size
|
123
|
+
value = "<span class='unsupported'>(object too large - modify #{CGI.escapeHTML(obj.class.to_s)}#inspect or raise BetterErrors.maximum_variable_inspect_size)</span>"
|
124
|
+
end
|
125
|
+
|
126
|
+
value
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module BetterErrors
|
2
|
+
module ExceptionExtension
|
3
|
+
prepend_features Exception
|
4
|
+
|
5
|
+
def set_backtrace(*)
|
6
|
+
if caller_locations.none? { |loc| loc.path == __FILE__ }
|
7
|
+
@__better_errors_bindings_stack = ::Kernel.binding.callers.drop(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def __better_errors_bindings_stack
|
14
|
+
@__better_errors_bindings_stack || []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require "json"
|
2
|
+
require "ipaddr"
|
3
|
+
require "set"
|
4
|
+
require "rack"
|
5
|
+
|
6
|
+
module BetterErrors
|
7
|
+
# Better Errors' error handling middleware. Including this in your middleware
|
8
|
+
# stack will show a Better Errors error page for exceptions raised below this
|
9
|
+
# middleware.
|
10
|
+
#
|
11
|
+
# If you are using Ruby on Rails, you do not need to manually insert this
|
12
|
+
# middleware into your middleware stack.
|
13
|
+
#
|
14
|
+
# @example Sinatra
|
15
|
+
# require "better_errors"
|
16
|
+
#
|
17
|
+
# if development?
|
18
|
+
# use BetterErrors::Middleware
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# @example Rack
|
22
|
+
# require "better_errors"
|
23
|
+
# if ENV["RACK_ENV"] == "development"
|
24
|
+
# use BetterErrors::Middleware
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class Middleware
|
28
|
+
# The set of IP addresses that are allowed to access Better Errors.
|
29
|
+
#
|
30
|
+
# Set to `{ "127.0.0.1/8", "::1/128" }` by default.
|
31
|
+
ALLOWED_IPS = Set.new
|
32
|
+
|
33
|
+
# Adds an address to the set of IP addresses allowed to access Better
|
34
|
+
# Errors.
|
35
|
+
def self.allow_ip!(addr)
|
36
|
+
ALLOWED_IPS << IPAddr.new(addr)
|
37
|
+
end
|
38
|
+
|
39
|
+
allow_ip! "127.0.0.0/8"
|
40
|
+
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
|
41
|
+
|
42
|
+
# A new instance of BetterErrors::Middleware
|
43
|
+
#
|
44
|
+
# @param app The Rack app/middleware to wrap with Better Errors
|
45
|
+
# @param handler The error handler to use.
|
46
|
+
def initialize(app, handler = ErrorPage)
|
47
|
+
@app = app
|
48
|
+
@handler = handler
|
49
|
+
end
|
50
|
+
|
51
|
+
# Calls the Better Errors middleware
|
52
|
+
#
|
53
|
+
# @param [Hash] env
|
54
|
+
# @return [Array]
|
55
|
+
def call(env)
|
56
|
+
if allow_ip? env
|
57
|
+
better_errors_call env
|
58
|
+
else
|
59
|
+
@app.call env
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def allow_ip?(env)
|
66
|
+
request = Rack::Request.new(env)
|
67
|
+
return true unless request.ip and !request.ip.strip.empty?
|
68
|
+
ip = IPAddr.new request.ip.split("%").first
|
69
|
+
ALLOWED_IPS.any? { |subnet| subnet.include? ip }
|
70
|
+
end
|
71
|
+
|
72
|
+
def better_errors_call(env)
|
73
|
+
case env["PATH_INFO"]
|
74
|
+
when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
|
75
|
+
internal_call env, $~
|
76
|
+
when %r{/__better_errors/?\z}
|
77
|
+
show_error_page env
|
78
|
+
else
|
79
|
+
protected_app_call env
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def protected_app_call(env)
|
84
|
+
@app.call env
|
85
|
+
rescue Exception => ex
|
86
|
+
@error_page = @handler.new ex, env
|
87
|
+
log_exception
|
88
|
+
show_error_page(env, ex)
|
89
|
+
end
|
90
|
+
|
91
|
+
def show_error_page(env, exception=nil)
|
92
|
+
type, content = if @error_page
|
93
|
+
if text?(env)
|
94
|
+
[ 'plain', @error_page.render('text') ]
|
95
|
+
else
|
96
|
+
[ 'html', @error_page.render ]
|
97
|
+
end
|
98
|
+
else
|
99
|
+
[ 'html', no_errors_page ]
|
100
|
+
end
|
101
|
+
|
102
|
+
status_code = 500
|
103
|
+
if defined? ActionDispatch::ExceptionWrapper
|
104
|
+
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
|
105
|
+
end
|
106
|
+
|
107
|
+
[status_code, { "Content-Type" => "text/#{type}; charset=utf-8" }, [content]]
|
108
|
+
end
|
109
|
+
|
110
|
+
def text?(env)
|
111
|
+
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" ||
|
112
|
+
!env["HTTP_ACCEPT"].to_s.include?('html')
|
113
|
+
end
|
114
|
+
|
115
|
+
def log_exception
|
116
|
+
return unless BetterErrors.logger
|
117
|
+
|
118
|
+
message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n"
|
119
|
+
@error_page.backtrace_frames.each do |frame|
|
120
|
+
message << " #{frame}\n"
|
121
|
+
end
|
122
|
+
|
123
|
+
BetterErrors.logger.fatal message
|
124
|
+
end
|
125
|
+
|
126
|
+
def internal_call(env, opts)
|
127
|
+
if opts[:id] != @error_page.id
|
128
|
+
return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]]
|
129
|
+
end
|
130
|
+
|
131
|
+
env["rack.input"].rewind
|
132
|
+
response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read))
|
133
|
+
[200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
|
134
|
+
end
|
135
|
+
|
136
|
+
def no_errors_page
|
137
|
+
"<h1>No errors</h1><p>No errors have been recorded yet.</p><hr>" +
|
138
|
+
"<code>Better Errors v#{BetterErrors::VERSION}</code>"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BetterErrors
|
2
|
+
# @private
|
3
|
+
class Railtie < Rails::Railtie
|
4
|
+
initializer "better_errors.configure_rails_initialization" do
|
5
|
+
if use_better_errors?
|
6
|
+
insert_middleware
|
7
|
+
BetterErrors.logger = Rails.logger
|
8
|
+
BetterErrors.application_root = Rails.root.to_s
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def insert_middleware
|
13
|
+
if defined? ActionDispatch::DebugExceptions
|
14
|
+
app.middleware.insert_after ActionDispatch::DebugExceptions, BetterErrors::Middleware
|
15
|
+
else
|
16
|
+
app.middleware.use BetterErrors::Middleware
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def use_better_errors?
|
21
|
+
!Rails.env.production? and app.config.consider_all_requests_local
|
22
|
+
end
|
23
|
+
|
24
|
+
def app
|
25
|
+
Rails.application
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# @private
|
2
|
+
module BetterErrors
|
3
|
+
class RaisedException
|
4
|
+
attr_reader :exception, :message, :backtrace
|
5
|
+
|
6
|
+
def initialize(exception)
|
7
|
+
if exception.respond_to?(:cause)
|
8
|
+
exception = exception.cause if exception.cause
|
9
|
+
elsif exception.respond_to?(:original_exception) && exception.original_exception
|
10
|
+
exception = exception.original_exception
|
11
|
+
end
|
12
|
+
|
13
|
+
@exception = exception
|
14
|
+
@message = exception.message
|
15
|
+
|
16
|
+
setup_backtrace
|
17
|
+
massage_syntax_error
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
exception.class
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def has_bindings?
|
26
|
+
exception.respond_to?(:__better_errors_bindings_stack) && exception.__better_errors_bindings_stack.any?
|
27
|
+
end
|
28
|
+
|
29
|
+
def setup_backtrace
|
30
|
+
if has_bindings?
|
31
|
+
setup_backtrace_from_bindings
|
32
|
+
else
|
33
|
+
setup_backtrace_from_backtrace
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup_backtrace_from_bindings
|
38
|
+
@backtrace = exception.__better_errors_bindings_stack.map { |binding|
|
39
|
+
file = binding.eval "__FILE__"
|
40
|
+
line = binding.eval "__LINE__"
|
41
|
+
name = binding.frame_description
|
42
|
+
StackFrame.new(file, line, name, binding)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def setup_backtrace_from_backtrace
|
47
|
+
@backtrace = (exception.backtrace || []).map { |frame|
|
48
|
+
if /\A(?<file>.*?):(?<line>\d+)(:in `(?<name>.*)')?/ =~ frame
|
49
|
+
StackFrame.new(file, line.to_i, name)
|
50
|
+
end
|
51
|
+
}.compact
|
52
|
+
end
|
53
|
+
|
54
|
+
def massage_syntax_error
|
55
|
+
case exception.class.to_s
|
56
|
+
when "Haml::SyntaxError", "Sprockets::Coffeelint::Error"
|
57
|
+
if /\A(.+?):(\d+)/ =~ exception.backtrace.first
|
58
|
+
backtrace.unshift(StackFrame.new($1, $2.to_i, ""))
|
59
|
+
end
|
60
|
+
when "SyntaxError"
|
61
|
+
if /\A(.+?):(\d+): (.*)/m =~ exception.message
|
62
|
+
backtrace.unshift(StackFrame.new($1, $2.to_i, ""))
|
63
|
+
@message = $3
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module BetterErrors
|
2
|
+
# @private
|
3
|
+
module REPL
|
4
|
+
PROVIDERS = [
|
5
|
+
{ impl: "better_errors/repl/basic",
|
6
|
+
const: :Basic },
|
7
|
+
]
|
8
|
+
|
9
|
+
def self.provider
|
10
|
+
@provider ||= const_get detect[:const]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.provider=(prov)
|
14
|
+
@provider = prov
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.detect
|
18
|
+
PROVIDERS.find { |prov|
|
19
|
+
test_provider prov
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.test_provider(provider)
|
24
|
+
require provider[:impl]
|
25
|
+
true
|
26
|
+
rescue LoadError
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|