Skip to content

Commit

Permalink
Add Rack::ErrorHandler for generic error handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Jan 13, 2023
1 parent 10aa25a commit 65782e6
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/rack.rb
Expand Up @@ -25,6 +25,7 @@ module Rack
autoload :ContentLength, "rack/content_length"
autoload :ContentType, "rack/content_type"
autoload :ETag, "rack/etag"
autoload :ErrorHandler, "rack/error_handler"
autoload :Events, "rack/events"
autoload :File, "rack/file"
autoload :Files, "rack/files"
Expand Down
40 changes: 40 additions & 0 deletions lib/rack/error_handler.rb
@@ -0,0 +1,40 @@
require_relative 'constants'

module Rack
class ErrorHandler
def initialize(app)
@app = app
end

def log_error(env, error)
if error_stream = env[RACK_ERRORS]
# Exception#full_message was introduced in Ruby 2.5+. If it's not
# available, we fall back to the old behavior of just printing the
# exception message.
if error.respond_to?(:full_message)
error_stream.puts(error.full_message)
else
error_stream.puts(error.message)
end
end
end

def bad_request(error)
[400, {}, ["Bad Request: #{error.class}"]]
end

def internal_server_error(error)
[500, {}, ["Internal Server Error: #{error.class}"]]
end

def call(env)
@app.call(env)
rescue BadRequest => error
log_error(env, error)
return bad_request(error)
rescue ScriptError, StandardError => error
log_error(env, error)
return internal_server_error(error)
end
end
end
58 changes: 58 additions & 0 deletions test/spec_error_handler.rb
@@ -0,0 +1,58 @@
# frozen_string_literal: true

require_relative 'helper'
require 'tempfile'
require 'fileutils'

separate_testing do
require_relative '../lib/rack/error_handler'
require_relative '../lib/rack/lint'
require_relative '../lib/rack/mock_request'
end

describe Rack::ErrorHandler do
it "catches bad requests as 400 Bad Request" do
error = Class.new(StandardError)
error.include(Rack::BadRequest)

app = lambda do |env|
raise error, "b00m"
end

middleware = Rack::Lint.new(Rack::ErrorHandler.new(app))
response = middleware.call(Rack::MockRequest.env_for("/"))

response[0].must_equal 400
response[2].to_ary.must_equal ["Bad Request: #{error}"]
end

it "catches standard errors as 500 Internal Server Error" do
error = Class.new(StandardError)

app = lambda do |env|
raise error, "b00m"
end

middleware = Rack::Lint.new(Rack::ErrorHandler.new(app))
response = middleware.call(Rack::MockRequest.env_for("/"))

response[0].must_equal 500
response[2].to_ary.must_equal ["Internal Server Error: #{error}"]
end

it "logs errors to the rack.errors stream" do
app = lambda do |env|
raise "b00m"
end

middleware = Rack::Lint.new(Rack::ErrorHandler.new(app))
errors = StringIO.new
response = middleware.call(Rack::MockRequest.env_for("/", "rack.errors" => errors))

errors.string.must_match(/RuntimeError/)
errors.string.must_match(/b00m/)

response[0].must_equal 500
response[2].to_ary.must_equal ["Internal Server Error: RuntimeError"]
end
end

0 comments on commit 65782e6

Please sign in to comment.