class BetterErrors::Middleware
Better Errors’ error handling middleware. Including this in your middleware stack will show a Better Errors error page for exceptions raised below this middleware.
If you are using Ruby on Rails, you do not need to manually insert this middleware into your middleware stack.
@example Sinatra
require "better_errors" if development? use BetterErrors::Middleware end
@example Rack
require "better_errors" if ENV["RACK_ENV"] == "development" use BetterErrors::Middleware end
Constants
- ALLOWED_IPS
The set of IP addresses that are allowed to access Better Errors.
Set to ‘{ “127.0.0.1/8”, “::1/128” }` by default.
- CSRF_TOKEN_COOKIE_NAME
Public Class Methods
allow_ip!(addr)
click to toggle source
Adds an address to the set of IP addresses allowed to access Better Errors.
# File lib/better_errors/middleware.rb, line 36 def self.allow_ip!(addr) ALLOWED_IPS << (addr.is_a?(IPAddr) ? addr : IPAddr.new(addr)) end
new(app, handler = ErrorPage)
click to toggle source
A new instance of BetterErrors::Middleware
@param app The Rack app/middleware to wrap with Better Errors @param handler The error handler to use.
# File lib/better_errors/middleware.rb, line 49 def initialize(app, handler = ErrorPage) @app = app @handler = handler end
Public Instance Methods
call(env)
click to toggle source
Calls the Better Errors middleware
@param [Hash] env @return [Array]
# File lib/better_errors/middleware.rb, line 58 def call(env) if allow_ip? env better_errors_call env else @app.call env end end
Private Instance Methods
allow_ip?(env)
click to toggle source
# File lib/better_errors/middleware.rb, line 68 def allow_ip?(env) request = Rack::Request.new(env) return true unless request.ip and !request.ip.strip.empty? ip = IPAddr.new request.ip.split("%").first ALLOWED_IPS.any? { |subnet| subnet.include? ip } end
backtrace_frames()
click to toggle source
# File lib/better_errors/middleware.rb, line 156 def backtrace_frames if defined?(Rails) && defined?(Rails.backtrace_cleaner) Rails.backtrace_cleaner.clean @error_page.backtrace_frames.map(&:to_s) else @error_page.backtrace_frames end end
better_errors_call(env)
click to toggle source
# File lib/better_errors/middleware.rb, line 75 def better_errors_call(env) case env["PATH_INFO"] when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z} internal_call(env, $~[:id], $~[:method]) when %r{/__better_errors/?\z} show_error_page env else protected_app_call env end end
internal_call(env, id, method)
click to toggle source
# File lib/better_errors/middleware.rb, line 164 def internal_call(env, id, method) return not_found_json_response unless %w[variables eval].include?(method) return no_errors_json_response unless @error_page return invalid_error_json_response if id != @error_page.id request = Rack::Request.new(env) return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME] request.body.rewind body = JSON.parse(request.body.read) return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME] == body['csrfToken'] return not_acceptable_json_response unless request.content_type == 'application/json' response = @error_page.send("do_#{method}", body) [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]] end
invalid_csrf_token_json_response()
click to toggle source
# File lib/better_errors/middleware.rb, line 215 def invalid_csrf_token_json_response [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump( error: "Invalid CSRF Token", explanation: "The browser session might have been cleared, " + "or something went wrong.", )]] end
invalid_error_json_response()
click to toggle source
# File lib/better_errors/middleware.rb, line 207 def invalid_error_json_response [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump( error: "Session expired", explanation: "This page was likely opened from a previous exception, " + "and the exception is no longer available in memory.", )]] end
log_exception()
click to toggle source
# File lib/better_errors/middleware.rb, line 147 def log_exception return unless BetterErrors.logger message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n" message += backtrace_frames.map { |frame| " #{frame}\n" }.join BetterErrors.logger.fatal message end
no_errors_json_response()
click to toggle source
# File lib/better_errors/middleware.rb, line 187 def no_errors_json_response explanation = if defined? Middleman "Middleman reloads all dependencies for each request, " + "which breaks Better Errors." elsif defined?(Shotgun) && defined?(Hanami) "Hanami is likely running with code-reloading enabled, which is the default. " + "You can disable this by running hanami with the `--no-code-reloading` option." elsif defined? Shotgun "The shotgun gem causes everything to be reloaded for every request. " + "You can disable shotgun in the Gemfile temporarily to use Better Errors." else "The application has been restarted since this page loaded, " + "or the framework is reloading all gems before each request " end [200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump( error: 'No exception information available', explanation: explanation, )]] end
no_errors_page()
click to toggle source
# File lib/better_errors/middleware.rb, line 182 def no_errors_page "<h1>No errors</h1><p>No errors have been recorded yet.</p><hr>" + "<code>Better Errors v#{BetterErrors::VERSION}</code>" end
not_acceptable_json_response()
click to toggle source
# File lib/better_errors/middleware.rb, line 230 def not_acceptable_json_response [406, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump( error: "Request not acceptable", explanation: "The internal request did not match an acceptable content type.", )]] end
not_found_json_response()
click to toggle source
# File lib/better_errors/middleware.rb, line 223 def not_found_json_response [404, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump( error: "Not found", explanation: "Not a recognized internal call.", )]] end
protected_app_call(env)
click to toggle source
# File lib/better_errors/middleware.rb, line 86 def protected_app_call(env) @app.call env rescue Exception => ex @error_page = @handler.new ex, env log_exception show_error_page(env, ex) end
show_error_page(env, exception=nil)
click to toggle source
# File lib/better_errors/middleware.rb, line 94 def show_error_page(env, exception=nil) request = Rack::Request.new(env) csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid csp_nonce = SecureRandom.base64(12) type, content = if @error_page if text?(env) [ 'plain', @error_page.render_text ] else [ 'html', @error_page.render_main(csrf_token, csp_nonce) ] end else [ 'html', no_errors_page ] end status_code = 500 if defined?(ActionDispatch::ExceptionWrapper) && exception status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code end headers = { "Content-Type" => "text/#{type}; charset=utf-8", "Content-Security-Policy" => [ "default-src 'none'", # Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set # for older browsers without nonce support. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src "script-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'", "style-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'", "img-src data:", "connect-src 'self'", "navigate-to 'self' #{BetterErrors.editor.scheme}", ].join('; '), } response = Rack::Response.new(content, status_code, headers) unless request.cookies[CSRF_TOKEN_COOKIE_NAME] response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict) end # In older versions of Rack, the body returned here is actually a Rack::BodyProxy which seems to be a bug. # (It contains status, headers and body and does not act like an array of strings.) # Since we already have status code and body here, there's no need to use the ones in the Rack::Response. (_status_code, headers, _body) = response.finish [status_code, headers, [content]] end
text?(env)
click to toggle source
# File lib/better_errors/middleware.rb, line 142 def text?(env) env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" || !env["HTTP_ACCEPT"].to_s.include?('html') end