hyper-pure-rb 0.0.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/better_errors-2.10.1/CHANGELOG.md +3 -0
- data/better_errors-2.10.1/Gemfile +11 -0
- data/better_errors-2.10.1/LICENSE +22 -0
- data/better_errors-2.10.1/README.md +163 -0
- data/better_errors-2.10.1/better_errors.gemspec +48 -0
- data/better_errors-2.10.1/gemfiles/pry010.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/pry011.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/pry09.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rack.gemfile +8 -0
- data/better_errors-2.10.1/gemfiles/rack_boc.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rails42.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/rails42_boc.gemfile +11 -0
- data/better_errors-2.10.1/gemfiles/rails42_haml.gemfile +11 -0
- data/better_errors-2.10.1/gemfiles/rails50.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rails50_boc.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/rails50_haml.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/rails51.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rails51_boc.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/rails51_haml.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/rails52.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rails52_boc.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/rails52_haml.gemfile +10 -0
- data/better_errors-2.10.1/gemfiles/rails60.gemfile +8 -0
- data/better_errors-2.10.1/gemfiles/rails60_boc.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rails60_haml.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rails61.gemfile +8 -0
- data/better_errors-2.10.1/gemfiles/rails61_boc.gemfile +9 -0
- data/better_errors-2.10.1/gemfiles/rails61_haml.gemfile +9 -0
- data/better_errors-2.10.1/lib/better_errors/code_formatter/html.rb +40 -0
- data/better_errors-2.10.1/lib/better_errors/code_formatter/text.rb +14 -0
- data/better_errors-2.10.1/lib/better_errors/code_formatter.rb +52 -0
- data/better_errors-2.10.1/lib/better_errors/editor.rb +103 -0
- data/better_errors-2.10.1/lib/better_errors/error_page.rb +169 -0
- data/better_errors-2.10.1/lib/better_errors/error_page_style.rb +43 -0
- data/better_errors-2.10.1/lib/better_errors/exception_extension.rb +17 -0
- data/better_errors-2.10.1/lib/better_errors/exception_hint.rb +29 -0
- data/better_errors-2.10.1/lib/better_errors/inspectable_value.rb +45 -0
- data/better_errors-2.10.1/lib/better_errors/middleware.rb +237 -0
- data/better_errors-2.10.1/lib/better_errors/rails.rb +28 -0
- data/better_errors-2.10.1/lib/better_errors/raised_exception.rb +89 -0
- data/better_errors-2.10.1/lib/better_errors/repl/basic.rb +20 -0
- data/better_errors-2.10.1/lib/better_errors/repl/pry.rb +88 -0
- data/better_errors-2.10.1/lib/better_errors/repl.rb +32 -0
- data/better_errors-2.10.1/lib/better_errors/stack_frame.rb +136 -0
- data/better_errors-2.10.1/lib/better_errors/templates/main.css +1 -0
- data/better_errors-2.10.1/lib/better_errors/templates/main.erb +431 -0
- data/better_errors-2.10.1/lib/better_errors/templates/text.erb +24 -0
- data/better_errors-2.10.1/lib/better_errors/templates/variable_info.erb +82 -0
- data/better_errors-2.10.1/lib/better_errors/version.rb +4 -0
- data/better_errors-2.10.1/lib/better_errors.rb +144 -0
- data/hyper-pure-rb.gemspec +11 -0
- metadata +91 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module BetterErrors
|
|
4
|
+
class Editor
|
|
5
|
+
KNOWN_EDITORS = [
|
|
6
|
+
{ symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
|
|
7
|
+
{ symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
|
|
8
|
+
{ symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
|
|
9
|
+
{ symbols: [:macvim, :mvim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
|
|
10
|
+
{ symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
|
|
11
|
+
{ symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
|
|
12
|
+
{ symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
|
|
13
|
+
{ symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
|
|
14
|
+
{ symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def self.for_formatting_string(formatting_string)
|
|
18
|
+
new proc { |file, line|
|
|
19
|
+
formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.for_proc(url_proc)
|
|
24
|
+
new url_proc
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Automatically sniffs a default editor preset based on
|
|
28
|
+
# environment variables.
|
|
29
|
+
#
|
|
30
|
+
# @return [Symbol]
|
|
31
|
+
def self.default_editor
|
|
32
|
+
editor_from_environment_formatting_string ||
|
|
33
|
+
editor_from_environment_editor ||
|
|
34
|
+
editor_from_symbol(:textmate)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.editor_from_environment_editor
|
|
38
|
+
if ENV["BETTER_ERRORS_EDITOR"]
|
|
39
|
+
editor = editor_from_command(ENV["BETTER_ERRORS_EDITOR"])
|
|
40
|
+
return editor if editor
|
|
41
|
+
puts "BETTER_ERRORS_EDITOR environment variable is not recognized as a supported Better Errors editor."
|
|
42
|
+
end
|
|
43
|
+
if ENV["EDITOR"]
|
|
44
|
+
editor = editor_from_command(ENV["EDITOR"])
|
|
45
|
+
return editor if editor
|
|
46
|
+
puts "EDITOR environment variable is not recognized as a supported Better Errors editor. Using TextMate by default."
|
|
47
|
+
else
|
|
48
|
+
puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default."
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.editor_from_command(editor_command)
|
|
53
|
+
env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] }
|
|
54
|
+
for_formatting_string(env_preset[:url]) if env_preset
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.editor_from_environment_formatting_string
|
|
58
|
+
return unless ENV['BETTER_ERRORS_EDITOR_URL']
|
|
59
|
+
|
|
60
|
+
for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL'])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.editor_from_symbol(symbol)
|
|
64
|
+
KNOWN_EDITORS.each do |preset|
|
|
65
|
+
return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def initialize(url_proc)
|
|
70
|
+
@url_proc = url_proc
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def url(raw_path, line)
|
|
74
|
+
if virtual_path && raw_path.start_with?(virtual_path)
|
|
75
|
+
if host_path
|
|
76
|
+
file = raw_path.sub(%r{\A#{virtual_path}}, host_path)
|
|
77
|
+
else
|
|
78
|
+
file = raw_path.sub(%r{\A#{virtual_path}/}, '')
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
file = raw_path
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
url_proc.call(file, line)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def scheme
|
|
88
|
+
url('/fake', 42).sub(/:.*/, ':')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
attr_reader :url_proc
|
|
94
|
+
|
|
95
|
+
def virtual_path
|
|
96
|
+
@virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH']
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def host_path
|
|
100
|
+
@host_path ||= ENV['BETTER_ERRORS_HOST_PATH']
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
require "json"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "rouge"
|
|
5
|
+
require "better_errors/error_page_style"
|
|
6
|
+
|
|
7
|
+
module BetterErrors
|
|
8
|
+
# @private
|
|
9
|
+
class ErrorPage
|
|
10
|
+
VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time)
|
|
11
|
+
|
|
12
|
+
def self.template_path(template_name)
|
|
13
|
+
File.expand_path("../templates/#{template_name}.erb", __FILE__)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.template(template_name)
|
|
17
|
+
Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.render_template(template_name, locals)
|
|
21
|
+
locals.send(:eval, self.template(template_name).src)
|
|
22
|
+
rescue => e
|
|
23
|
+
# Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
|
|
24
|
+
# We don't know the line number, so just injecting the template path has to be enough.
|
|
25
|
+
e.backtrace.unshift "#{self.template_path(template_name)}:0"
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :exception, :env, :repls
|
|
30
|
+
|
|
31
|
+
def initialize(exception, env)
|
|
32
|
+
@exception = RaisedException.new(exception)
|
|
33
|
+
@env = env
|
|
34
|
+
@start_time = Time.now.to_f
|
|
35
|
+
@repls = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def id
|
|
39
|
+
@id ||= SecureRandom.hex(8)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_main(csrf_token, csp_nonce)
|
|
43
|
+
frame = backtrace_frames[0]
|
|
44
|
+
first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
|
|
45
|
+
self.class.render_template('main', binding)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_text
|
|
49
|
+
self.class.render_template('text', binding)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def do_variables(opts)
|
|
53
|
+
index = opts["index"].to_i
|
|
54
|
+
frame = backtrace_frames[index]
|
|
55
|
+
variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
|
|
56
|
+
{ html: self.class.render_template("variable_info", variable_info) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def do_eval(opts)
|
|
60
|
+
index = opts["index"].to_i
|
|
61
|
+
code = opts["source"]
|
|
62
|
+
|
|
63
|
+
unless (binding = backtrace_frames[index].frame_binding)
|
|
64
|
+
return { error: "REPL unavailable in this stack frame" }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@repls[index] ||= REPL.provider.new(binding, exception)
|
|
68
|
+
|
|
69
|
+
eval_and_respond(index, code)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def backtrace_frames
|
|
73
|
+
exception.backtrace
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def exception_type
|
|
77
|
+
exception.type
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def exception_message
|
|
81
|
+
exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def exception_hint
|
|
85
|
+
exception.hint
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def active_support_actions
|
|
89
|
+
return [] unless defined?(ActiveSupport::ActionableError)
|
|
90
|
+
|
|
91
|
+
ActiveSupport::ActionableError.actions(exception.type)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def action_dispatch_action_endpoint
|
|
95
|
+
return unless defined?(ActionDispatch::ActionableExceptions)
|
|
96
|
+
|
|
97
|
+
ActionDispatch::ActionableExceptions.endpoint
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def application_frames
|
|
101
|
+
backtrace_frames.select(&:application?)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def first_frame
|
|
105
|
+
application_frames.first || backtrace_frames.first
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def editor_url(frame)
|
|
111
|
+
BetterErrors.editor.url(frame.filename, frame.line)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def rack_session
|
|
115
|
+
env['rack.session']
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def rails_params
|
|
119
|
+
env['action_dispatch.request.parameters']
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def uri_prefix
|
|
123
|
+
env["SCRIPT_NAME"] || ""
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def request_path
|
|
127
|
+
env["PATH_INFO"]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.html_formatted_code_block(frame)
|
|
131
|
+
CodeFormatter::HTML.new(frame.filename, frame.line).output
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.text_formatted_code_block(frame)
|
|
135
|
+
CodeFormatter::Text.new(frame.filename, frame.line).output
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def text_heading(char, str)
|
|
139
|
+
str + "\n" + char*str.size
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.inspect_value(obj)
|
|
143
|
+
if BetterErrors.ignored_classes.include? obj.class.name
|
|
144
|
+
"<span class='unsupported'>(Instance of ignored class. "\
|
|
145
|
+
"#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
|
|
146
|
+
" BetterErrors.ignored_classes if you need to see it.)</span>"
|
|
147
|
+
else
|
|
148
|
+
InspectableValue.new(obj).to_html
|
|
149
|
+
end
|
|
150
|
+
rescue BetterErrors::ValueLargerThanConfiguredMaximum
|
|
151
|
+
"<span class='unsupported'>(Object too large. "\
|
|
152
|
+
"#{obj.class.name ? "Modify #{CGI.escapeHTML(obj.class.name)}#inspect or a" : "A"}"\
|
|
153
|
+
"djust BetterErrors.maximum_variable_inspect_size if you need to see it.)</span>"
|
|
154
|
+
rescue Exception => e
|
|
155
|
+
"<span class='unsupported'>(exception #{CGI.escapeHTML(e.class.to_s)} was raised in inspect)</span>"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def eval_and_respond(index, code)
|
|
159
|
+
result, prompt, prefilled_input = @repls[index].send_input(code)
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
highlighted_input: Rouge::Formatters::HTML.new.format(Rouge::Lexers::Ruby.lex(code)),
|
|
163
|
+
prefilled_input: prefilled_input,
|
|
164
|
+
prompt: prompt,
|
|
165
|
+
result: result
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module BetterErrors
|
|
2
|
+
# @private
|
|
3
|
+
module ErrorPageStyle
|
|
4
|
+
def self.compiled_css(for_deployment = false)
|
|
5
|
+
begin
|
|
6
|
+
require "sassc"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
raise LoadError, "The `sassc` gem is required when developing the `better_errors` gem. "\
|
|
9
|
+
"If you're using a release of `better_errors`, the compiled CSS is missing from the released gem"
|
|
10
|
+
# If you arrived here because sassc is not in your project's Gemfile,
|
|
11
|
+
# the issue here is that the release of the better_errors gem
|
|
12
|
+
# is supposed to contain the compiled CSS, but that file is missing from the release.
|
|
13
|
+
# So better_errors is trying to build the CSS on the fly, which requires the sassc gem.
|
|
14
|
+
#
|
|
15
|
+
# If you're developing the better_errors gem locally, and you're running a project
|
|
16
|
+
# that does not have sassc in its bundle, run `rake style:build` in the better_errors
|
|
17
|
+
# project to compile the CSS file.
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
style_dir = File.expand_path("style", File.dirname(__FILE__))
|
|
21
|
+
style_file = "#{style_dir}/main.scss"
|
|
22
|
+
|
|
23
|
+
engine = SassC::Engine.new(
|
|
24
|
+
File.read(style_file),
|
|
25
|
+
filename: style_file,
|
|
26
|
+
style: for_deployment ? :compressed : :expanded,
|
|
27
|
+
line_comments: !for_deployment,
|
|
28
|
+
load_paths: [style_dir],
|
|
29
|
+
)
|
|
30
|
+
engine.render
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.style_tag(csp_nonce)
|
|
34
|
+
style_file = File.expand_path("templates/main.css", File.dirname(__FILE__))
|
|
35
|
+
css = if File.exist?(style_file)
|
|
36
|
+
File.open(style_file).read
|
|
37
|
+
else
|
|
38
|
+
compiled_css(false)
|
|
39
|
+
end
|
|
40
|
+
"<style type='text/css' nonce='#{csp_nonce}'>\n#{css}\n</style>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
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,29 @@
|
|
|
1
|
+
module BetterErrors
|
|
2
|
+
class ExceptionHint
|
|
3
|
+
def initialize(exception)
|
|
4
|
+
@exception = exception
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def hint
|
|
8
|
+
case exception
|
|
9
|
+
when NoMethodError
|
|
10
|
+
/\Aundefined method `(?<method>[^']+)' for (?<val>[^:]+):(?<klass>\w+)/.match(exception.message) do |match|
|
|
11
|
+
if match[:val] == "nil"
|
|
12
|
+
return "Something is `nil` when it probably shouldn't be."
|
|
13
|
+
elsif !match[:klass].start_with? '0x'
|
|
14
|
+
return "`#{match[:method]}` is being called on a `#{match[:klass]}` object, "\
|
|
15
|
+
"which might not be the type of object you were expecting."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
when NameError
|
|
19
|
+
/\Aundefined local variable or method `(?<method>[^']+)' for/.match(exception.message) do |match|
|
|
20
|
+
return "`#{match[:method]}` is probably misspelled."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :exception
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
require "objspace" rescue nil
|
|
3
|
+
|
|
4
|
+
module BetterErrors
|
|
5
|
+
class ValueLargerThanConfiguredMaximum < StandardError; end
|
|
6
|
+
|
|
7
|
+
class InspectableValue
|
|
8
|
+
def initialize(value)
|
|
9
|
+
@original_value = value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_html
|
|
13
|
+
raise ValueLargerThanConfiguredMaximum unless value_small_enough_to_inspect?
|
|
14
|
+
value_as_html
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :original_value
|
|
20
|
+
|
|
21
|
+
def value_as_html
|
|
22
|
+
@value_as_html ||= CGI.escapeHTML(value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def value
|
|
26
|
+
@value ||= begin
|
|
27
|
+
if original_value.respond_to? :inspect
|
|
28
|
+
original_value.inspect
|
|
29
|
+
else
|
|
30
|
+
original_value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def value_small_enough_to_inspect?
|
|
36
|
+
return true if BetterErrors.maximum_variable_inspect_size.nil?
|
|
37
|
+
|
|
38
|
+
if defined?(ObjectSpace) && defined?(ObjectSpace.memsize_of) && ObjectSpace.memsize_of(value)
|
|
39
|
+
ObjectSpace.memsize_of(value) <= BetterErrors.maximum_variable_inspect_size
|
|
40
|
+
else
|
|
41
|
+
value_as_html.length <= BetterErrors.maximum_variable_inspect_size
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "ipaddr"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "set"
|
|
5
|
+
require "rack"
|
|
6
|
+
|
|
7
|
+
module BetterErrors
|
|
8
|
+
# Better Errors' error handling middleware. Including this in your middleware
|
|
9
|
+
# stack will show a Better Errors error page for exceptions raised below this
|
|
10
|
+
# middleware.
|
|
11
|
+
#
|
|
12
|
+
# If you are using Ruby on Rails, you do not need to manually insert this
|
|
13
|
+
# middleware into your middleware stack.
|
|
14
|
+
#
|
|
15
|
+
# @example Sinatra
|
|
16
|
+
# require "better_errors"
|
|
17
|
+
#
|
|
18
|
+
# if development?
|
|
19
|
+
# use BetterErrors::Middleware
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Rack
|
|
23
|
+
# require "better_errors"
|
|
24
|
+
# if ENV["RACK_ENV"] == "development"
|
|
25
|
+
# use BetterErrors::Middleware
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
class Middleware
|
|
29
|
+
# The set of IP addresses that are allowed to access Better Errors.
|
|
30
|
+
#
|
|
31
|
+
# Set to `{ "127.0.0.1/8", "::1/128" }` by default.
|
|
32
|
+
ALLOWED_IPS = Set.new
|
|
33
|
+
|
|
34
|
+
# Adds an address to the set of IP addresses allowed to access Better
|
|
35
|
+
# Errors.
|
|
36
|
+
def self.allow_ip!(addr)
|
|
37
|
+
ALLOWED_IPS << (addr.is_a?(IPAddr) ? addr : IPAddr.new(addr))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
allow_ip! "127.0.0.0/8"
|
|
41
|
+
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
|
|
42
|
+
|
|
43
|
+
CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token"
|
|
44
|
+
|
|
45
|
+
# A new instance of BetterErrors::Middleware
|
|
46
|
+
#
|
|
47
|
+
# @param app The Rack app/middleware to wrap with Better Errors
|
|
48
|
+
# @param handler The error handler to use.
|
|
49
|
+
def initialize(app, handler = ErrorPage)
|
|
50
|
+
@app = app
|
|
51
|
+
@handler = handler
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Calls the Better Errors middleware
|
|
55
|
+
#
|
|
56
|
+
# @param [Hash] env
|
|
57
|
+
# @return [Array]
|
|
58
|
+
def call(env)
|
|
59
|
+
if allow_ip? env
|
|
60
|
+
better_errors_call env
|
|
61
|
+
else
|
|
62
|
+
@app.call env
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def allow_ip?(env)
|
|
69
|
+
request = Rack::Request.new(env)
|
|
70
|
+
return true unless request.ip and !request.ip.strip.empty?
|
|
71
|
+
ip = IPAddr.new request.ip.split("%").first
|
|
72
|
+
ALLOWED_IPS.any? { |subnet| subnet.include? ip }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def better_errors_call(env)
|
|
76
|
+
case env["PATH_INFO"]
|
|
77
|
+
when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
|
|
78
|
+
internal_call(env, $~[:id], $~[:method])
|
|
79
|
+
when %r{/__better_errors/?\z}
|
|
80
|
+
show_error_page env
|
|
81
|
+
else
|
|
82
|
+
protected_app_call env
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def protected_app_call(env)
|
|
87
|
+
@app.call env
|
|
88
|
+
rescue Exception => ex
|
|
89
|
+
@error_page = @handler.new ex, env
|
|
90
|
+
log_exception
|
|
91
|
+
show_error_page(env, ex)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def show_error_page(env, exception=nil)
|
|
95
|
+
request = Rack::Request.new(env)
|
|
96
|
+
csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid
|
|
97
|
+
csp_nonce = SecureRandom.base64(12)
|
|
98
|
+
|
|
99
|
+
type, content = if @error_page
|
|
100
|
+
if text?(env)
|
|
101
|
+
[ 'plain', @error_page.render_text ]
|
|
102
|
+
else
|
|
103
|
+
[ 'html', @error_page.render_main(csrf_token, csp_nonce) ]
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
[ 'html', no_errors_page ]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
status_code = 500
|
|
110
|
+
if defined?(ActionDispatch::ExceptionWrapper) && exception
|
|
111
|
+
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
headers = {
|
|
115
|
+
"Content-Type" => "text/#{type}; charset=utf-8",
|
|
116
|
+
"Content-Security-Policy" => [
|
|
117
|
+
"default-src 'none'",
|
|
118
|
+
# Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set
|
|
119
|
+
# for older browsers without nonce support.
|
|
120
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
|
|
121
|
+
"script-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
|
|
122
|
+
"style-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
|
|
123
|
+
"img-src data:",
|
|
124
|
+
"connect-src 'self'",
|
|
125
|
+
"navigate-to 'self' #{BetterErrors.editor.scheme}",
|
|
126
|
+
].join('; '),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
response = Rack::Response.new(content, status_code, headers)
|
|
130
|
+
|
|
131
|
+
unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
|
|
132
|
+
response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# In older versions of Rack, the body returned here is actually a Rack::BodyProxy which seems to be a bug.
|
|
136
|
+
# (It contains status, headers and body and does not act like an array of strings.)
|
|
137
|
+
# Since we already have status code and body here, there's no need to use the ones in the Rack::Response.
|
|
138
|
+
(_status_code, headers, _body) = response.finish
|
|
139
|
+
[status_code, headers, [content]]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def text?(env)
|
|
143
|
+
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" ||
|
|
144
|
+
!env["HTTP_ACCEPT"].to_s.include?('html')
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def log_exception
|
|
148
|
+
return unless BetterErrors.logger
|
|
149
|
+
|
|
150
|
+
message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n"
|
|
151
|
+
message += backtrace_frames.map { |frame| " #{frame}\n" }.join
|
|
152
|
+
|
|
153
|
+
BetterErrors.logger.fatal message
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def backtrace_frames
|
|
157
|
+
if defined?(Rails) && defined?(Rails.backtrace_cleaner)
|
|
158
|
+
Rails.backtrace_cleaner.clean @error_page.backtrace_frames.map(&:to_s)
|
|
159
|
+
else
|
|
160
|
+
@error_page.backtrace_frames
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def internal_call(env, id, method)
|
|
165
|
+
return not_found_json_response unless %w[variables eval].include?(method)
|
|
166
|
+
return no_errors_json_response unless @error_page
|
|
167
|
+
return invalid_error_json_response if id != @error_page.id
|
|
168
|
+
|
|
169
|
+
request = Rack::Request.new(env)
|
|
170
|
+
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
|
|
171
|
+
|
|
172
|
+
request.body.rewind
|
|
173
|
+
body = JSON.parse(request.body.read)
|
|
174
|
+
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME] == body['csrfToken']
|
|
175
|
+
|
|
176
|
+
return not_acceptable_json_response unless request.content_type == 'application/json'
|
|
177
|
+
|
|
178
|
+
response = @error_page.send("do_#{method}", body)
|
|
179
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def no_errors_page
|
|
183
|
+
"<h1>No errors</h1><p>No errors have been recorded yet.</p><hr>" +
|
|
184
|
+
"<code>Better Errors v#{BetterErrors::VERSION}</code>"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def no_errors_json_response
|
|
188
|
+
explanation = if defined? Middleman
|
|
189
|
+
"Middleman reloads all dependencies for each request, " +
|
|
190
|
+
"which breaks Better Errors."
|
|
191
|
+
elsif defined?(Shotgun) && defined?(Hanami)
|
|
192
|
+
"Hanami is likely running with code-reloading enabled, which is the default. " +
|
|
193
|
+
"You can disable this by running hanami with the `--no-code-reloading` option."
|
|
194
|
+
elsif defined? Shotgun
|
|
195
|
+
"The shotgun gem causes everything to be reloaded for every request. " +
|
|
196
|
+
"You can disable shotgun in the Gemfile temporarily to use Better Errors."
|
|
197
|
+
else
|
|
198
|
+
"The application has been restarted since this page loaded, " +
|
|
199
|
+
"or the framework is reloading all gems before each request "
|
|
200
|
+
end
|
|
201
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
202
|
+
error: 'No exception information available',
|
|
203
|
+
explanation: explanation,
|
|
204
|
+
)]]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def invalid_error_json_response
|
|
208
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
209
|
+
error: "Session expired",
|
|
210
|
+
explanation: "This page was likely opened from a previous exception, " +
|
|
211
|
+
"and the exception is no longer available in memory.",
|
|
212
|
+
)]]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def invalid_csrf_token_json_response
|
|
216
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
217
|
+
error: "Invalid CSRF Token",
|
|
218
|
+
explanation: "The browser session might have been cleared, " +
|
|
219
|
+
"or something went wrong.",
|
|
220
|
+
)]]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def not_found_json_response
|
|
224
|
+
[404, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
225
|
+
error: "Not found",
|
|
226
|
+
explanation: "Not a recognized internal call.",
|
|
227
|
+
)]]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def not_acceptable_json_response
|
|
231
|
+
[406, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
232
|
+
error: "Request not acceptable",
|
|
233
|
+
explanation: "The internal request did not match an acceptable content type.",
|
|
234
|
+
)]]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
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
|