better_errors-creditkudos 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +8 -0
  4. data/.yardopts +1 -0
  5. data/CHANGELOG.md +3 -0
  6. data/Gemfile +11 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +128 -0
  9. data/Rakefile +13 -0
  10. data/better_errors-creditkudos.gemspec +28 -0
  11. data/lib/better_errors.rb +152 -0
  12. data/lib/better_errors/code_formatter.rb +63 -0
  13. data/lib/better_errors/code_formatter/html.rb +26 -0
  14. data/lib/better_errors/code_formatter/text.rb +14 -0
  15. data/lib/better_errors/error_page.rb +129 -0
  16. data/lib/better_errors/exception_extension.rb +17 -0
  17. data/lib/better_errors/middleware.rb +141 -0
  18. data/lib/better_errors/rails.rb +28 -0
  19. data/lib/better_errors/raised_exception.rb +68 -0
  20. data/lib/better_errors/repl.rb +30 -0
  21. data/lib/better_errors/repl/basic.rb +20 -0
  22. data/lib/better_errors/repl/pry.rb +78 -0
  23. data/lib/better_errors/stack_frame.rb +111 -0
  24. data/lib/better_errors/templates/main.erb +1032 -0
  25. data/lib/better_errors/templates/text.erb +21 -0
  26. data/lib/better_errors/templates/variable_info.erb +72 -0
  27. data/lib/better_errors/version.rb +3 -0
  28. data/spec/better_errors/code_formatter_spec.rb +92 -0
  29. data/spec/better_errors/error_page_spec.rb +122 -0
  30. data/spec/better_errors/middleware_spec.rb +180 -0
  31. data/spec/better_errors/raised_exception_spec.rb +72 -0
  32. data/spec/better_errors/repl/basic_spec.rb +18 -0
  33. data/spec/better_errors/repl/pry_spec.rb +40 -0
  34. data/spec/better_errors/repl/shared_examples.rb +18 -0
  35. data/spec/better_errors/stack_frame_spec.rb +157 -0
  36. data/spec/better_errors/support/my_source.rb +20 -0
  37. data/spec/better_errors_spec.rb +73 -0
  38. data/spec/spec_helper.rb +5 -0
  39. data/spec/without_binding_of_caller.rb +9 -0
  40. 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