better_errors 2.8.1 → 2.10.0.beta1
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 +4 -4
- data/.github/workflows/ci.yml +142 -0
- data/.github/workflows/release.yml +64 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -1
- data/README.md +2 -2
- data/gemfiles/pry010.gemfile +2 -1
- data/gemfiles/pry011.gemfile +2 -1
- data/gemfiles/pry09.gemfile +2 -1
- data/gemfiles/rack.gemfile +2 -1
- data/gemfiles/rack_boc.gemfile +2 -1
- data/gemfiles/rails42.gemfile +2 -1
- data/gemfiles/rails42_boc.gemfile +2 -1
- data/gemfiles/rails42_haml.gemfile +2 -1
- data/gemfiles/rails50.gemfile +2 -1
- data/gemfiles/rails50_boc.gemfile +2 -1
- data/gemfiles/rails50_haml.gemfile +2 -1
- data/gemfiles/rails51.gemfile +2 -1
- data/gemfiles/rails51_boc.gemfile +2 -1
- data/gemfiles/rails51_haml.gemfile +2 -1
- data/gemfiles/rails52.gemfile +2 -1
- data/gemfiles/rails52_boc.gemfile +2 -1
- data/gemfiles/rails52_haml.gemfile +2 -1
- data/gemfiles/rails60.gemfile +2 -1
- data/gemfiles/rails60_boc.gemfile +2 -1
- data/gemfiles/rails60_haml.gemfile +2 -1
- data/gemfiles/rails61.gemfile +8 -0
- data/gemfiles/rails61_boc.gemfile +9 -0
- data/gemfiles/rails61_haml.gemfile +9 -0
- data/lib/better_errors.rb +15 -34
- data/lib/better_errors/editor.rb +103 -0
- data/lib/better_errors/error_page.rb +30 -14
- data/lib/better_errors/exception_hint.rb +29 -0
- data/lib/better_errors/middleware.rb +21 -5
- data/lib/better_errors/raised_exception.rb +8 -1
- data/lib/better_errors/templates/main.erb +111 -28
- data/lib/better_errors/templates/text.erb +6 -3
- data/lib/better_errors/templates/variable_info.erb +18 -15
- data/lib/better_errors/version.rb +1 -1
- metadata +13 -6
- data/.travis.yml +0 -111
data/lib/better_errors.rb
CHANGED
|
@@ -3,6 +3,7 @@ require "erubi"
|
|
|
3
3
|
require "coderay"
|
|
4
4
|
require "uri"
|
|
5
5
|
|
|
6
|
+
require "better_errors/version"
|
|
6
7
|
require "better_errors/code_formatter"
|
|
7
8
|
require "better_errors/inspectable_value"
|
|
8
9
|
require "better_errors/error_page"
|
|
@@ -10,21 +11,9 @@ require "better_errors/middleware"
|
|
|
10
11
|
require "better_errors/raised_exception"
|
|
11
12
|
require "better_errors/repl"
|
|
12
13
|
require "better_errors/stack_frame"
|
|
13
|
-
require "better_errors/
|
|
14
|
+
require "better_errors/editor"
|
|
14
15
|
|
|
15
16
|
module BetterErrors
|
|
16
|
-
POSSIBLE_EDITOR_PRESETS = [
|
|
17
|
-
{ symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
|
|
18
|
-
{ symbols: [:macvim, :mvim], sniff: /vim/i, url: proc { |file, line| "mvim://open?url=file://#{file}&line=#{line}" } },
|
|
19
|
-
{ symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
|
|
20
|
-
{ symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
|
|
21
|
-
{ symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
|
|
22
|
-
{ symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
|
|
23
|
-
{ symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
|
|
24
|
-
{ symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
|
|
25
|
-
{ symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
|
|
26
|
-
]
|
|
27
|
-
|
|
28
17
|
class << self
|
|
29
18
|
# The path to the root of the application. Better Errors uses this property
|
|
30
19
|
# to determine if a file in a backtrace should be considered an application
|
|
@@ -64,17 +53,18 @@ module BetterErrors
|
|
|
64
53
|
@maximum_variable_inspect_size = 100_000
|
|
65
54
|
@ignored_classes = ['ActionDispatch::Request', 'ActionDispatch::Response']
|
|
66
55
|
|
|
67
|
-
# Returns
|
|
56
|
+
# Returns an object which responds to #url, which when called with
|
|
57
|
+
# a filename and line number argument,
|
|
68
58
|
# returns a URL to open the filename and line in the selected editor.
|
|
69
59
|
#
|
|
70
60
|
# Generates TextMate URLs by default.
|
|
71
61
|
#
|
|
72
|
-
# BetterErrors.editor
|
|
62
|
+
# BetterErrors.editor.url("/some/file", 123)
|
|
73
63
|
# # => txmt://open?url=file:///some/file&line=123
|
|
74
64
|
#
|
|
75
65
|
# @return [Proc]
|
|
76
66
|
def self.editor
|
|
77
|
-
@editor
|
|
67
|
+
@editor ||= default_editor
|
|
78
68
|
end
|
|
79
69
|
|
|
80
70
|
# Configures how Better Errors generates open-in-editor URLs.
|
|
@@ -115,20 +105,15 @@ module BetterErrors
|
|
|
115
105
|
# @param [Proc] proc
|
|
116
106
|
#
|
|
117
107
|
def self.editor=(editor)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
self.editor = proc { |file, line| editor % { file: URI.encode_www_form_component(file), line: line } }
|
|
108
|
+
if editor.is_a? Symbol
|
|
109
|
+
@editor = Editor.editor_from_symbol(editor)
|
|
110
|
+
raise(ArgumentError, "Symbol #{editor} is not a symbol in the list of supported errors.") unless editor
|
|
111
|
+
elsif editor.is_a? String
|
|
112
|
+
@editor = Editor.for_formatting_string(editor)
|
|
113
|
+
elsif editor.respond_to? :call
|
|
114
|
+
@editor = Editor.for_proc(editor)
|
|
126
115
|
else
|
|
127
|
-
|
|
128
|
-
@editor = editor
|
|
129
|
-
else
|
|
130
|
-
raise TypeError, "Expected editor to be a valid editor key, a format string or a callable."
|
|
131
|
-
end
|
|
116
|
+
raise ArgumentError, "Expected editor to be a valid editor key, a format string or a callable."
|
|
132
117
|
end
|
|
133
118
|
end
|
|
134
119
|
|
|
@@ -145,12 +130,8 @@ module BetterErrors
|
|
|
145
130
|
#
|
|
146
131
|
# @return [Symbol]
|
|
147
132
|
def self.default_editor
|
|
148
|
-
|
|
149
|
-
ENV["EDITOR"] =~ config[:sniff]
|
|
150
|
-
}[:url] || :textmate
|
|
133
|
+
Editor.default_editor
|
|
151
134
|
end
|
|
152
|
-
|
|
153
|
-
BetterErrors.editor = default_editor
|
|
154
135
|
end
|
|
155
136
|
|
|
156
137
|
begin
|
|
@@ -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
|
|
@@ -5,6 +5,8 @@ require "securerandom"
|
|
|
5
5
|
module BetterErrors
|
|
6
6
|
# @private
|
|
7
7
|
class ErrorPage
|
|
8
|
+
VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time)
|
|
9
|
+
|
|
8
10
|
def self.template_path(template_name)
|
|
9
11
|
File.expand_path("../templates/#{template_name}.erb", __FILE__)
|
|
10
12
|
end
|
|
@@ -13,6 +15,15 @@ module BetterErrors
|
|
|
13
15
|
Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
def self.render_template(template_name, locals)
|
|
19
|
+
locals.send(:eval, self.template(template_name).src)
|
|
20
|
+
rescue => e
|
|
21
|
+
# Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
|
|
22
|
+
# We don't know the line number, so just injecting the template path has to be enough.
|
|
23
|
+
e.backtrace.unshift "#{self.template_path(template_name)}:0"
|
|
24
|
+
raise
|
|
25
|
+
end
|
|
26
|
+
|
|
16
27
|
attr_reader :exception, :env, :repls
|
|
17
28
|
|
|
18
29
|
def initialize(exception, env)
|
|
@@ -26,20 +37,21 @@ module BetterErrors
|
|
|
26
37
|
@id ||= SecureRandom.hex(8)
|
|
27
38
|
end
|
|
28
39
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
def render_main(csrf_token, csp_nonce)
|
|
41
|
+
frame = backtrace_frames[0]
|
|
42
|
+
first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
|
|
43
|
+
self.class.render_template('main', binding)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render_text
|
|
47
|
+
self.class.render_template('text', binding)
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
def do_variables(opts)
|
|
39
51
|
index = opts["index"].to_i
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{ html:
|
|
52
|
+
frame = backtrace_frames[index]
|
|
53
|
+
variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
|
|
54
|
+
{ html: self.class.render_template("variable_info", variable_info) }
|
|
43
55
|
end
|
|
44
56
|
|
|
45
57
|
def do_eval(opts)
|
|
@@ -67,6 +79,10 @@ module BetterErrors
|
|
|
67
79
|
exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
|
|
68
80
|
end
|
|
69
81
|
|
|
82
|
+
def exception_hint
|
|
83
|
+
exception.hint
|
|
84
|
+
end
|
|
85
|
+
|
|
70
86
|
def active_support_actions
|
|
71
87
|
return [] unless defined?(ActiveSupport::ActionableError)
|
|
72
88
|
|
|
@@ -90,7 +106,7 @@ module BetterErrors
|
|
|
90
106
|
private
|
|
91
107
|
|
|
92
108
|
def editor_url(frame)
|
|
93
|
-
BetterErrors.editor
|
|
109
|
+
BetterErrors.editor.url(frame.filename, frame.line)
|
|
94
110
|
end
|
|
95
111
|
|
|
96
112
|
def rack_session
|
|
@@ -109,11 +125,11 @@ module BetterErrors
|
|
|
109
125
|
env["PATH_INFO"]
|
|
110
126
|
end
|
|
111
127
|
|
|
112
|
-
def html_formatted_code_block(frame)
|
|
128
|
+
def self.html_formatted_code_block(frame)
|
|
113
129
|
CodeFormatter::HTML.new(frame.filename, frame.line).output
|
|
114
130
|
end
|
|
115
131
|
|
|
116
|
-
def text_formatted_code_block(frame)
|
|
132
|
+
def self.text_formatted_code_block(frame)
|
|
117
133
|
CodeFormatter::Text.new(frame.filename, frame.line).output
|
|
118
134
|
end
|
|
119
135
|
|
|
@@ -121,7 +137,7 @@ module BetterErrors
|
|
|
121
137
|
str + "\n" + char*str.size
|
|
122
138
|
end
|
|
123
139
|
|
|
124
|
-
def inspect_value(obj)
|
|
140
|
+
def self.inspect_value(obj)
|
|
125
141
|
if BetterErrors.ignored_classes.include? obj.class.name
|
|
126
142
|
"<span class='unsupported'>(Instance of ignored class. "\
|
|
127
143
|
"#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
|
|
@@ -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
|
|
@@ -40,7 +40,7 @@ module BetterErrors
|
|
|
40
40
|
allow_ip! "127.0.0.0/8"
|
|
41
41
|
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
|
|
42
42
|
|
|
43
|
-
CSRF_TOKEN_COOKIE_NAME =
|
|
43
|
+
CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token"
|
|
44
44
|
|
|
45
45
|
# A new instance of BetterErrors::Middleware
|
|
46
46
|
#
|
|
@@ -94,12 +94,13 @@ module BetterErrors
|
|
|
94
94
|
def show_error_page(env, exception=nil)
|
|
95
95
|
request = Rack::Request.new(env)
|
|
96
96
|
csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid
|
|
97
|
+
csp_nonce = SecureRandom.base64(12)
|
|
97
98
|
|
|
98
99
|
type, content = if @error_page
|
|
99
100
|
if text?(env)
|
|
100
|
-
[ 'plain', @error_page.
|
|
101
|
+
[ 'plain', @error_page.render_text ]
|
|
101
102
|
else
|
|
102
|
-
[ 'html', @error_page.
|
|
103
|
+
[ 'html', @error_page.render_main(csrf_token, csp_nonce) ]
|
|
103
104
|
end
|
|
104
105
|
else
|
|
105
106
|
[ 'html', no_errors_page ]
|
|
@@ -110,10 +111,25 @@ module BetterErrors
|
|
|
110
111
|
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
|
|
111
112
|
end
|
|
112
113
|
|
|
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
|
+
# Inline style is required by the syntax highlighter.
|
|
123
|
+
"style-src 'self' 'unsafe-inline'",
|
|
124
|
+
"connect-src 'self'",
|
|
125
|
+
"navigate-to 'self' #{BetterErrors.editor.scheme}",
|
|
126
|
+
].join('; '),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
response = Rack::Response.new(content, status_code, headers)
|
|
114
130
|
|
|
115
131
|
unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
|
|
116
|
-
response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, httponly: true, same_site: :strict)
|
|
132
|
+
response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict)
|
|
117
133
|
end
|
|
118
134
|
|
|
119
135
|
# In older versions of Rack, the body returned here is actually a Rack::BodyProxy which seems to be a bug.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
require 'better_errors/exception_hint'
|
|
2
|
+
|
|
1
3
|
# @private
|
|
2
4
|
module BetterErrors
|
|
3
5
|
class RaisedException
|
|
4
|
-
attr_reader :exception, :message, :backtrace
|
|
6
|
+
attr_reader :exception, :message, :backtrace, :hint
|
|
5
7
|
|
|
6
8
|
def initialize(exception)
|
|
7
9
|
if exception.class.name == "ActionView::Template::Error" && exception.respond_to?(:cause)
|
|
@@ -23,6 +25,7 @@ module BetterErrors
|
|
|
23
25
|
@message = exception.message
|
|
24
26
|
|
|
25
27
|
setup_backtrace
|
|
28
|
+
setup_hint
|
|
26
29
|
massage_syntax_error
|
|
27
30
|
end
|
|
28
31
|
|
|
@@ -78,5 +81,9 @@ module BetterErrors
|
|
|
78
81
|
end
|
|
79
82
|
end
|
|
80
83
|
end
|
|
84
|
+
|
|
85
|
+
def setup_hint
|
|
86
|
+
@hint = ExceptionHint.new(exception).hint
|
|
87
|
+
end
|
|
81
88
|
end
|
|
82
89
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<title><%= exception_type %> at <%= request_path %></title>
|
|
5
5
|
</head>
|
|
6
|
-
<body>
|
|
6
|
+
<body class="better-errors-javascript-not-loaded">
|
|
7
7
|
<%# Stylesheets are placed in the <body> for Turbolinks compatibility. %>
|
|
8
8
|
<style>
|
|
9
9
|
/* Basic reset */
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
nav.sidebar,
|
|
91
91
|
.frame_info {
|
|
92
92
|
position: fixed;
|
|
93
|
-
top:
|
|
93
|
+
top: 102px;
|
|
94
94
|
bottom: 0;
|
|
95
95
|
|
|
96
96
|
box-sizing: border-box;
|
|
@@ -102,11 +102,13 @@
|
|
|
102
102
|
nav.sidebar {
|
|
103
103
|
width: 40%;
|
|
104
104
|
left: 20px;
|
|
105
|
-
top:
|
|
105
|
+
top: 122px;
|
|
106
106
|
bottom: 20px;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
.frame_info {
|
|
110
|
+
display: none;
|
|
111
|
+
|
|
110
112
|
right: 0;
|
|
111
113
|
left: 40%;
|
|
112
114
|
|
|
@@ -114,6 +116,9 @@
|
|
|
114
116
|
padding-left: 10px;
|
|
115
117
|
margin-left: 30px;
|
|
116
118
|
}
|
|
119
|
+
.frame_info.current {
|
|
120
|
+
display: block;
|
|
121
|
+
}
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
nav.sidebar {
|
|
@@ -131,7 +136,7 @@
|
|
|
131
136
|
header.exception {
|
|
132
137
|
padding: 18px 20px;
|
|
133
138
|
|
|
134
|
-
height:
|
|
139
|
+
height: 66px;
|
|
135
140
|
min-height: 59px;
|
|
136
141
|
|
|
137
142
|
overflow: hidden;
|
|
@@ -227,6 +232,10 @@
|
|
|
227
232
|
* Navigation
|
|
228
233
|
* --------------------------------------------------------------------- */
|
|
229
234
|
|
|
235
|
+
.better-errors-javascript-not-loaded .backtrace .tabs {
|
|
236
|
+
display: none;
|
|
237
|
+
}
|
|
238
|
+
|
|
230
239
|
nav.tabs {
|
|
231
240
|
border-bottom: solid 1px #ddd;
|
|
232
241
|
|
|
@@ -411,6 +420,18 @@
|
|
|
411
420
|
* Display area
|
|
412
421
|
* --------------------------------------------------------------------- */
|
|
413
422
|
|
|
423
|
+
p.no-javascript-notice {
|
|
424
|
+
margin-bottom: 1em;
|
|
425
|
+
padding: 1em;
|
|
426
|
+
border: 2px solid #e00;
|
|
427
|
+
}
|
|
428
|
+
.better-errors-javascript-loaded .no-javascript-notice {
|
|
429
|
+
display: none;
|
|
430
|
+
}
|
|
431
|
+
.no-inline-style-notice {
|
|
432
|
+
display: none;
|
|
433
|
+
}
|
|
434
|
+
|
|
414
435
|
.trace_info {
|
|
415
436
|
background: #fff;
|
|
416
437
|
padding: 6px;
|
|
@@ -468,6 +489,10 @@
|
|
|
468
489
|
font-weight: 200;
|
|
469
490
|
}
|
|
470
491
|
|
|
492
|
+
.better-errors-javascript-not-loaded .be-repl {
|
|
493
|
+
display: none;
|
|
494
|
+
}
|
|
495
|
+
|
|
471
496
|
.code, .be-console, .unavailable {
|
|
472
497
|
background: #fff;
|
|
473
498
|
padding: 5px;
|
|
@@ -595,6 +620,12 @@
|
|
|
595
620
|
color: #8080a0;
|
|
596
621
|
padding-left: 20px;
|
|
597
622
|
}
|
|
623
|
+
.console-has-been-used .live-console-hint {
|
|
624
|
+
display: none;
|
|
625
|
+
}
|
|
626
|
+
.better-errors-javascript-not-loaded .live-console-hint {
|
|
627
|
+
display: none;
|
|
628
|
+
}
|
|
598
629
|
|
|
599
630
|
.hint:before {
|
|
600
631
|
content: '\25b2';
|
|
@@ -611,17 +642,6 @@
|
|
|
611
642
|
margin: 10px 0;
|
|
612
643
|
}
|
|
613
644
|
|
|
614
|
-
.sub:before {
|
|
615
|
-
content: '';
|
|
616
|
-
display: block;
|
|
617
|
-
width: 100%;
|
|
618
|
-
height: 4px;
|
|
619
|
-
|
|
620
|
-
border-radius: 2px;
|
|
621
|
-
background: rgba(0, 150, 200, 0.05);
|
|
622
|
-
box-shadow: 1px 1px 0 rgba(255, 255, 255, 0.7), inset 0 0 0 1px rgba(0, 0, 0, 0.04), inset 2px 2px 2px rgba(0, 0, 0, 0.07);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
645
|
.sub h3 {
|
|
626
646
|
color: #39a;
|
|
627
647
|
font-size: 1.1em;
|
|
@@ -709,7 +729,7 @@
|
|
|
709
729
|
</style>
|
|
710
730
|
|
|
711
731
|
<%# IE8 compatibility crap %>
|
|
712
|
-
<script>
|
|
732
|
+
<script nonce="<%= csp_nonce %>">
|
|
713
733
|
(function() {
|
|
714
734
|
var elements = ["section", "nav", "header", "footer", "audio"];
|
|
715
735
|
for (var i = 0; i < elements.length; i++) {
|
|
@@ -723,7 +743,7 @@
|
|
|
723
743
|
rendered in the host app's layout. Let's empty out the styles of the
|
|
724
744
|
host app.
|
|
725
745
|
%>
|
|
726
|
-
<script>
|
|
746
|
+
<script nonce="<%= csp_nonce %>">
|
|
727
747
|
if (window.Turbolinks) {
|
|
728
748
|
for(var i=0; i < document.styleSheets.length; i++) {
|
|
729
749
|
if(document.styleSheets[i].href)
|
|
@@ -748,6 +768,15 @@
|
|
|
748
768
|
}
|
|
749
769
|
</script>
|
|
750
770
|
|
|
771
|
+
<p class='no-inline-style-notice'>
|
|
772
|
+
<strong>
|
|
773
|
+
Better Errors can't apply inline style<span class='no-javascript-notice'> (or run Javascript)</span>,
|
|
774
|
+
possibly because you have a Content Security Policy along with Turbolinks.
|
|
775
|
+
But you can
|
|
776
|
+
<a href='/__better_errors' target="_blank">open the interactive console in a new tab/window</a>.
|
|
777
|
+
</strong>
|
|
778
|
+
</p>
|
|
779
|
+
|
|
751
780
|
<div class='top'>
|
|
752
781
|
<header class="exception">
|
|
753
782
|
<h2><strong><%= exception_type %></strong> <span>at <%= request_path %></span></h2>
|
|
@@ -764,6 +793,9 @@
|
|
|
764
793
|
<% end %>
|
|
765
794
|
</div>
|
|
766
795
|
<% end %>
|
|
796
|
+
<% if exception_hint %>
|
|
797
|
+
<h2>Hint: <%= exception_hint %></h2>
|
|
798
|
+
<% end %>
|
|
767
799
|
</header>
|
|
768
800
|
</div>
|
|
769
801
|
|
|
@@ -791,21 +823,37 @@
|
|
|
791
823
|
</ul>
|
|
792
824
|
</nav>
|
|
793
825
|
|
|
794
|
-
|
|
795
|
-
<div class="frame_info
|
|
796
|
-
|
|
826
|
+
<div class="frameInfos">
|
|
827
|
+
<div class="frame_info current" data-frame-idx="0">
|
|
828
|
+
<p class='no-javascript-notice'>
|
|
829
|
+
Better Errors can't run Javascript here<span class='no-inline-style-notice'> (or apply inline style)</span>,
|
|
830
|
+
possibly because you have a Content Security Policy along with Turbolinks.
|
|
831
|
+
But you can
|
|
832
|
+
<a href='/__better_errors' target="_blank">open the interactive console in a new tab/window</a>.
|
|
833
|
+
</p>
|
|
834
|
+
<!-- this is enough information to show something in case JS doesn't get to load -->
|
|
835
|
+
<%== ErrorPage.render_template('variable_info', first_frame_variable_info) %>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
797
838
|
</section>
|
|
798
839
|
</body>
|
|
799
|
-
<script>
|
|
840
|
+
<script nonce="<%= csp_nonce %>">
|
|
800
841
|
(function() {
|
|
801
842
|
|
|
802
843
|
var OID = "<%= id %>";
|
|
803
844
|
var csrfToken = "<%= csrf_token %>";
|
|
804
845
|
|
|
805
846
|
var previousFrame = null;
|
|
806
|
-
var previousFrameInfo = null;
|
|
807
847
|
var allFrames = document.querySelectorAll("ul.frames li");
|
|
808
|
-
var
|
|
848
|
+
var frameInfos = document.querySelector(".frameInfos");
|
|
849
|
+
|
|
850
|
+
document.querySelector('body').classList.remove("better-errors-javascript-not-loaded");
|
|
851
|
+
document.querySelector('body').classList.add("better-errors-javascript-loaded");
|
|
852
|
+
|
|
853
|
+
var noJSNotices = document.querySelectorAll('.no-javascript-notice');
|
|
854
|
+
for(var i = 0; i < noJSNotices.length; i++) {
|
|
855
|
+
noJSNotices[i].remove();
|
|
856
|
+
}
|
|
809
857
|
|
|
810
858
|
function apiCall(method, opts, cb) {
|
|
811
859
|
var req = new XMLHttpRequest();
|
|
@@ -825,6 +873,28 @@
|
|
|
825
873
|
return html.replace(/&/, "&").replace(/</g, "<");
|
|
826
874
|
}
|
|
827
875
|
|
|
876
|
+
function hasConsoleBeenUsedPreviously() {
|
|
877
|
+
return !!document.cookie.split('; ').find(function(cookie) {
|
|
878
|
+
return cookie.startsWith('BetterErrors-has-used-console=');
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
var consoleHasBeenUsed = hasConsoleBeenUsedPreviously();
|
|
883
|
+
|
|
884
|
+
function consoleWasJustUsed() {
|
|
885
|
+
if (consoleHasBeenUsed) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
hideConsoleHint();
|
|
890
|
+
consoleHasBeenUsed = true;
|
|
891
|
+
document.cookie = "BetterErrors-has-used-console=true;path=/;max-age=31536000;samesite"
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function hideConsoleHint() {
|
|
895
|
+
document.querySelector('body').className += " console-has-been-used";
|
|
896
|
+
}
|
|
897
|
+
|
|
828
898
|
function REPL(index) {
|
|
829
899
|
this.index = index;
|
|
830
900
|
|
|
@@ -846,15 +916,20 @@
|
|
|
846
916
|
this.inputElement = this.container.querySelector("input");
|
|
847
917
|
this.outputElement = this.container.querySelector("pre");
|
|
848
918
|
|
|
919
|
+
if (consoleHasBeenUsed) {
|
|
920
|
+
hideConsoleHint();
|
|
921
|
+
}
|
|
922
|
+
|
|
849
923
|
var self = this;
|
|
850
924
|
this.inputElement.onkeydown = function(ev) {
|
|
851
925
|
self.onKeyDown(ev);
|
|
926
|
+
consoleWasJustUsed();
|
|
852
927
|
};
|
|
853
928
|
|
|
854
929
|
this.setPrompt(">>");
|
|
855
930
|
|
|
856
931
|
REPL.all[this.index] = this;
|
|
857
|
-
}
|
|
932
|
+
};
|
|
858
933
|
|
|
859
934
|
REPL.prototype.focus = function() {
|
|
860
935
|
this.inputElement.focus();
|
|
@@ -952,17 +1027,25 @@
|
|
|
952
1027
|
};
|
|
953
1028
|
|
|
954
1029
|
function switchTo(el) {
|
|
955
|
-
|
|
956
|
-
|
|
1030
|
+
var currentFrameInfo = document.querySelectorAll('.frame_info.current');
|
|
1031
|
+
for(var i = 0; i < currentFrameInfo.length; i++) {
|
|
1032
|
+
currentFrameInfo[i].className = "frame_info";
|
|
1033
|
+
}
|
|
957
1034
|
|
|
958
|
-
el.
|
|
1035
|
+
el.className = "frame_info current";
|
|
959
1036
|
|
|
960
1037
|
var replInput = el.querySelector('.be-console input');
|
|
961
1038
|
if (replInput) replInput.focus();
|
|
962
1039
|
}
|
|
963
1040
|
|
|
964
1041
|
function selectFrameInfo(index) {
|
|
965
|
-
var el =
|
|
1042
|
+
var el = document.querySelector(".frame_info[data-frame-idx='" + index + "']")
|
|
1043
|
+
if (!el) {
|
|
1044
|
+
el = document.createElement("div");
|
|
1045
|
+
el.className = "frame_info";
|
|
1046
|
+
el.setAttribute('data-frame-idx', index);
|
|
1047
|
+
frameInfos.appendChild(el);
|
|
1048
|
+
}
|
|
966
1049
|
if(el) {
|
|
967
1050
|
if (el.loaded) {
|
|
968
1051
|
return switchTo(el);
|