Follow up - bca16dee16
- Enhancement: Provide unique error code for technical errors to make it easy to find them in the logs.
- Show all error messages if `current_user` has `admin` permission(s) right away. - Pass messages of `ActionController::RoutingError`, `ActiveRecord::RecordNotFound`, `Exceptions::UnprocessableEntity` and `Exceptions::NotAuthorized` exceptions to user. - Fix regression: 'Not authorized' errors lack of cause. - Remove diverged behavior for different environments.
This commit is contained in:
parent
584680e7e4
commit
bb7950255e
4 changed files with 79 additions and 54 deletions
|
@ -32,7 +32,7 @@ module ApplicationController::HandlesErrors
|
||||||
end
|
end
|
||||||
|
|
||||||
def unauthorized(e)
|
def unauthorized(e)
|
||||||
error = humanize_error(e.message)
|
error = humanize_error(e)
|
||||||
response.headers['X-Failure'] = error.fetch(:error_human, error[:error])
|
response.headers['X-Failure'] = error.fetch(:error_human, error[:error])
|
||||||
respond_to_exception(e, :unauthorized)
|
respond_to_exception(e, :unauthorized)
|
||||||
http_log
|
http_log
|
||||||
|
@ -44,9 +44,9 @@ module ApplicationController::HandlesErrors
|
||||||
status_code = Rack::Utils.status_code(status)
|
status_code = Rack::Utils.status_code(status)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json { render json: humanize_error(e.message), status: status }
|
format.json { render json: humanize_error(e), status: status }
|
||||||
format.any do
|
format.any do
|
||||||
errors = humanize_error(e.message)
|
errors = humanize_error(e)
|
||||||
@exception = e
|
@exception = e
|
||||||
@message = errors[:error_human] || errors[:error] || param[:message]
|
@message = errors[:error_human] || errors[:error] || param[:message]
|
||||||
@traceback = !Rails.env.production?
|
@traceback = !Rails.env.production?
|
||||||
|
@ -56,29 +56,30 @@ module ApplicationController::HandlesErrors
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def humanize_error(error)
|
def humanize_error(e)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
error: error
|
error: e.message
|
||||||
}
|
}
|
||||||
|
|
||||||
case error
|
if e.message =~ /Validation failed: (.+?)(,|$)/i
|
||||||
when /Validation failed: (.+?)(,|$)/i
|
|
||||||
data[:error_human] = $1
|
data[:error_human] = $1
|
||||||
when /(already exists|duplicate key|duplicate entry)/i
|
elsif e.message.match?(/(already exists|duplicate key|duplicate entry)/i)
|
||||||
data[:error_human] = 'Object already exists!'
|
data[:error_human] = 'Object already exists!'
|
||||||
when /null value in column "(.+?)" violates not-null constraint/i
|
elsif e.message =~ /null value in column "(.+?)" violates not-null constraint/i
|
||||||
data[:error_human] = "Attribute '#{$1}' required!"
|
data[:error_human] = "Attribute '#{$1}' required!"
|
||||||
when /Field '(.+?)' doesn't have a default value/i
|
elsif e.message =~ /Field '(.+?)' doesn't have a default value/i
|
||||||
data[:error_human] = "Attribute '#{$1}' required!"
|
data[:error_human] = "Attribute '#{$1}' required!"
|
||||||
when 'Exceptions::NotAuthorized'
|
elsif e.message == 'Exceptions::NotAuthorized'
|
||||||
data[:error] = 'Not authorized'
|
data[:error] = 'Not authorized'
|
||||||
data[:error_human] = data[:error]
|
data[:error_human] = data[:error]
|
||||||
|
elsif [ActionController::RoutingError, ActiveRecord::RecordNotFound, Exceptions::UnprocessableEntity, Exceptions::NotAuthorized].include?(e.class)
|
||||||
|
data[:error_human] = data[:error]
|
||||||
end
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
|
||||||
if data[:error_human].present?
|
if data[:error_human].present?
|
||||||
data[:error] = data.delete(:error_human)
|
data[:error] = data[:error_human]
|
||||||
else
|
elsif !current_user&.permissions?('admin')
|
||||||
# We want to avoid leaking of internal information but also want the user
|
# We want to avoid leaking of internal information but also want the user
|
||||||
# to give the administrator a reference to find the cause of the error.
|
# to give the administrator a reference to find the cause of the error.
|
||||||
# Therefore we generate a one time unique error ID that can be used to
|
# Therefore we generate a one time unique error ID that can be used to
|
||||||
|
@ -87,7 +88,7 @@ module ApplicationController::HandlesErrors
|
||||||
Rails.logger.error "#{error_code_prefix} #{data[:error]}"
|
Rails.logger.error "#{error_code_prefix} #{data[:error]}"
|
||||||
data[:error] = "#{error_code_prefix} Please contact your administrator."
|
data[:error] = "#{error_code_prefix} Please contact your administrator."
|
||||||
end
|
end
|
||||||
end
|
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,7 +51,7 @@ returns
|
||||||
|
|
||||||
# complain if we found no reference
|
# complain if we found no reference
|
||||||
if !lookup
|
if !lookup
|
||||||
raise ArgumentError, "No value found for '#{assoc_name}' with id #{item_id.inspect}"
|
raise Exceptions::UnprocessableEntity, "No value found for '#{assoc_name}' with id #{item_id.inspect}"
|
||||||
end
|
end
|
||||||
|
|
||||||
list.push item_id
|
list.push item_id
|
||||||
|
@ -94,7 +94,7 @@ returns
|
||||||
|
|
||||||
# complain if we found no reference
|
# complain if we found no reference
|
||||||
if !lookup
|
if !lookup
|
||||||
raise ArgumentError, "No lookup value found for '#{assoc_name}': #{value.inspect}"
|
raise Exceptions::UnprocessableEntity, "No lookup value found for '#{assoc_name}': #{value.inspect}"
|
||||||
end
|
end
|
||||||
|
|
||||||
list.push lookup.id
|
list.push lookup.id
|
||||||
|
@ -363,7 +363,7 @@ returns
|
||||||
lookup = nil
|
lookup = nil
|
||||||
if class_object == User
|
if class_object == User
|
||||||
if !value.instance_of?(String)
|
if !value.instance_of?(String)
|
||||||
raise ArgumentError, "String is needed as ref value #{value.inspect} for '#{assoc_name}'"
|
raise Exceptions::UnprocessableEntity, "String is needed as ref value #{value.inspect} for '#{assoc_name}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
if !lookup
|
if !lookup
|
||||||
|
@ -378,7 +378,7 @@ returns
|
||||||
|
|
||||||
# complain if we found no reference
|
# complain if we found no reference
|
||||||
if !lookup
|
if !lookup
|
||||||
raise ArgumentError, "No lookup value found for '#{assoc_name}': #{value.inspect}"
|
raise Exceptions::UnprocessableEntity, "No lookup value found for '#{assoc_name}': #{value.inspect}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# release data value
|
# release data value
|
||||||
|
@ -408,7 +408,7 @@ returns
|
||||||
lookup = nil
|
lookup = nil
|
||||||
if class_object == User
|
if class_object == User
|
||||||
if !item.instance_of?(String)
|
if !item.instance_of?(String)
|
||||||
raise ArgumentError, "String is needed in array ref as ref value #{value.inspect} for '#{assoc_name}'"
|
raise Exceptions::UnprocessableEntity, "String is needed in array ref as ref value #{value.inspect} for '#{assoc_name}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
if !lookup
|
if !lookup
|
||||||
|
@ -423,7 +423,7 @@ returns
|
||||||
|
|
||||||
# complain if we found no reference
|
# complain if we found no reference
|
||||||
if !lookup
|
if !lookup
|
||||||
raise ArgumentError, "No lookup value found for '#{assoc_name}': #{item.inspect}"
|
raise Exceptions::UnprocessableEntity, "No lookup value found for '#{assoc_name}': #{item.inspect}"
|
||||||
end
|
end
|
||||||
|
|
||||||
lookup_ids.push lookup.id
|
lookup_ids.push lookup.id
|
||||||
|
|
|
@ -28,7 +28,7 @@ returns
|
||||||
end
|
end
|
||||||
|
|
||||||
if params.nil?
|
if params.nil?
|
||||||
raise ArgumentError, "No params for #{self}!"
|
raise Exceptions::UnprocessableEntity, "No params for #{self}!"
|
||||||
end
|
end
|
||||||
|
|
||||||
# cleanup each member of array
|
# cleanup each member of array
|
||||||
|
@ -61,7 +61,7 @@ returns
|
||||||
next if data[name].blank?
|
next if data[name].blank?
|
||||||
next if assoc.klass.lookup(id: data[name])
|
next if assoc.klass.lookup(id: data[name])
|
||||||
|
|
||||||
raise ArgumentError, "Invalid value for param '#{name}': #{data[name].inspect}"
|
raise Exceptions::UnprocessableEntity, "Invalid value for param '#{name}': #{data[name].inspect}"
|
||||||
end
|
end
|
||||||
clean_params[attribute] = data[attribute]
|
clean_params[attribute] = data[attribute]
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ RSpec.describe 'Error handling', type: :request do
|
||||||
# a random error code that can be easily found in the logs by an
|
# a random error code that can be easily found in the logs by an
|
||||||
# administrator. However, this makes it hard to check for the exact error
|
# administrator. However, this makes it hard to check for the exact error
|
||||||
# message. Therefore we only check for the substring in this particular case
|
# message. Therefore we only check for the substring in this particular case
|
||||||
if message == 'Please contact your administrator'
|
if message == 'Please contact your administrator' || message == 'Mysql2::Error' || message == 'PG::ForeignKeyViolation'
|
||||||
expect(json_response['error']).to include(message)
|
expect(json_response['error']).to include(message)
|
||||||
else
|
else
|
||||||
expect(json_response['error']).to eq(message)
|
expect(json_response['error']).to eq(message)
|
||||||
|
@ -33,25 +33,40 @@ RSpec.describe 'Error handling', type: :request do
|
||||||
|
|
||||||
context 'error with confidential message is raised' do
|
context 'error with confidential message is raised' do
|
||||||
|
|
||||||
let(:admin_user) { create(:admin_user, groups: Group.all) }
|
|
||||||
let!(:ticket) { create(:ticket) }
|
let!(:ticket) { create(:ticket) }
|
||||||
let(:invalid_group_id) { 99_999 }
|
let(:invalid_group_id) { 99_999 }
|
||||||
let(:message) { 'Please contact your administrator' }
|
|
||||||
let(:http_status) { :unprocessable_entity }
|
let(:http_status) { :unprocessable_entity }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
# fake production ENV to enable error hiding
|
authenticated_as(requesting_user)
|
||||||
env = double(
|
|
||||||
production?: true,
|
|
||||||
test?: false,
|
|
||||||
development?: false
|
|
||||||
)
|
|
||||||
allow(::Rails).to receive(:env).and_return(env)
|
|
||||||
|
|
||||||
authenticated_as(admin_user)
|
|
||||||
put "/api/v1/tickets/#{ticket.id}?all=true", params: { group_id: invalid_group_id }, as: as
|
put "/api/v1/tickets/#{ticket.id}?all=true", params: { group_id: invalid_group_id }, as: as
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'agent user' do
|
||||||
|
let(:requesting_user) { create(:agent_user, groups: Group.all) }
|
||||||
|
let(:message) { 'Please contact your administrator' }
|
||||||
|
|
||||||
|
context 'requesting JSON' do
|
||||||
|
include_examples 'JSON response format'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'requesting HTML' do
|
||||||
|
let(:title) { '422: Unprocessable Entity' }
|
||||||
|
let(:headline) { '422: The change you wanted was rejected.' }
|
||||||
|
|
||||||
|
include_examples 'HTML response format'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'admin user' do
|
||||||
|
let(:requesting_user) { create(:admin_user, groups: Group.all) }
|
||||||
|
|
||||||
|
if ActiveRecord::Base.connection_config[:adapter] == 'mysql2'
|
||||||
|
let(:message) { 'Mysql2::Error' }
|
||||||
|
else
|
||||||
|
let(:message) { 'PG::ForeignKeyViolation' }
|
||||||
|
end
|
||||||
|
|
||||||
context 'requesting JSON' do
|
context 'requesting JSON' do
|
||||||
include_examples 'JSON response format'
|
include_examples 'JSON response format'
|
||||||
end
|
end
|
||||||
|
@ -63,6 +78,7 @@ RSpec.describe 'Error handling', type: :request do
|
||||||
include_examples 'HTML response format'
|
include_examples 'HTML response format'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'URL route does not exist' do
|
context 'URL route does not exist' do
|
||||||
|
|
||||||
|
@ -112,12 +128,12 @@ RSpec.describe 'Error handling', type: :request do
|
||||||
get '/tests/raised_exception', params: { exception: exception.name, message: message }, as: as
|
get '/tests/raised_exception', params: { exception: exception.name, message: message }, as: as
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'handles exception' do |exception, http_status, title, headline|
|
shared_examples 'exception check' do |message, exception, http_status, title, headline|
|
||||||
|
|
||||||
context "#{exception} is raised" do
|
context "#{exception} is raised" do
|
||||||
let(:exception) { exception }
|
let(:exception) { exception }
|
||||||
let(:http_status) { http_status }
|
let(:http_status) { http_status }
|
||||||
let(:message) { 'some error message' }
|
let(:message) { message }
|
||||||
|
|
||||||
context 'requesting JSON' do
|
context 'requesting JSON' do
|
||||||
include_examples 'JSON response format'
|
include_examples 'JSON response format'
|
||||||
|
@ -132,10 +148,18 @@ RSpec.describe 'Error handling', type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples 'handles exception', ActiveRecord::RecordNotFound, :not_found, '404: Not Found', '404: Requested resource was not found'
|
shared_examples 'handles exception' do |exception, http_status, title, headline|
|
||||||
|
include_examples 'exception check', 'some error message', exception, http_status, title, headline
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'masks exception' do |exception, http_status, title, headline|
|
||||||
|
include_examples 'exception check', 'Please contact your administrator', exception, http_status, title, headline
|
||||||
|
end
|
||||||
|
|
||||||
include_examples 'handles exception', Exceptions::NotAuthorized, :unauthorized, '401: Unauthorized', '401: Unauthorized'
|
include_examples 'handles exception', Exceptions::NotAuthorized, :unauthorized, '401: Unauthorized', '401: Unauthorized'
|
||||||
|
include_examples 'handles exception', ActiveRecord::RecordNotFound, :not_found, '404: Not Found', '404: Requested resource was not found'
|
||||||
include_examples 'handles exception', Exceptions::UnprocessableEntity, :unprocessable_entity, '422: Unprocessable Entity', '422: The change you wanted was rejected.'
|
include_examples 'handles exception', Exceptions::UnprocessableEntity, :unprocessable_entity, '422: Unprocessable Entity', '422: The change you wanted was rejected.'
|
||||||
include_examples 'handles exception', ArgumentError, :unprocessable_entity, '422: Unprocessable Entity', '422: The change you wanted was rejected.'
|
include_examples 'masks exception', ArgumentError, :unprocessable_entity, '422: Unprocessable Entity', '422: The change you wanted was rejected.'
|
||||||
include_examples 'handles exception', StandardError, :internal_server_error, '500: Something went wrong', "500: We're sorry, but something went wrong."
|
include_examples 'masks exception', StandardError, :internal_server_error, '500: Something went wrong', "500: We're sorry, but something went wrong."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue