2026-01-02 13:41:09 +00:00
|
|
|
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
2021-06-01 12:20:20 +00:00
|
|
|
|
2017-03-09 11:44:51 +00:00
|
|
|
module ApplicationController::HandlesErrors
|
2017-01-20 09:45:19 +00:00
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
|
|
|
|
included do
|
|
|
|
|
rescue_from StandardError, with: :internal_server_error
|
2021-07-12 09:46:57 +00:00
|
|
|
rescue_from 'ExecJS::RuntimeError', with: :internal_server_error
|
2017-01-20 09:45:19 +00:00
|
|
|
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
2026-04-22 15:23:21 +00:00
|
|
|
rescue_from ActiveRecord::StatementInvalid, with: :unprocessable_content
|
|
|
|
|
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_content
|
|
|
|
|
rescue_from ActiveRecord::DeleteRestrictionError, with: :unprocessable_content
|
|
|
|
|
rescue_from ArgumentError, with: :unprocessable_content
|
|
|
|
|
rescue_from Exceptions::UnprocessableContent, with: :unprocessable_content
|
2017-01-20 09:45:19 +00:00
|
|
|
rescue_from Exceptions::NotAuthorized, with: :unauthorized
|
2021-02-04 08:28:41 +00:00
|
|
|
rescue_from Exceptions::Forbidden, with: :forbidden
|
2020-03-19 09:39:51 +00:00
|
|
|
rescue_from Pundit::NotAuthorizedError, with: :pundit_not_authorized_error
|
2026-04-22 15:23:21 +00:00
|
|
|
rescue_from 'Store::Provider::S3::Error', with: :unprocessable_content
|
|
|
|
|
rescue_from Exceptions::MissingAttribute, Exceptions::InvalidAttribute, ActionController::ParameterMissing, with: :unprocessable_content
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def not_found(e)
|
2017-04-19 10:09:54 +00:00
|
|
|
logger.error e
|
2017-01-20 09:45:19 +00:00
|
|
|
respond_to_exception(e, :not_found)
|
2017-08-21 23:13:19 +00:00
|
|
|
http_log
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
|
|
|
|
|
2026-04-22 15:23:21 +00:00
|
|
|
def unprocessable_content(e)
|
2017-04-19 10:09:54 +00:00
|
|
|
logger.error e
|
2026-04-22 15:23:21 +00:00
|
|
|
respond_to_exception(e, :unprocessable_content)
|
2017-08-21 23:13:19 +00:00
|
|
|
http_log
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def internal_server_error(e)
|
2017-04-19 10:09:54 +00:00
|
|
|
logger.error e
|
2017-01-20 09:45:19 +00:00
|
|
|
respond_to_exception(e, :internal_server_error)
|
2017-08-21 23:13:19 +00:00
|
|
|
http_log
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def unauthorized(e)
|
2020-03-19 09:39:51 +00:00
|
|
|
logger.info { e }
|
2020-03-06 14:31:43 +00:00
|
|
|
error = humanize_error(e)
|
2017-01-20 09:45:19 +00:00
|
|
|
response.headers['X-Failure'] = error.fetch(:error_human, error[:error])
|
|
|
|
|
respond_to_exception(e, :unauthorized)
|
2017-08-21 23:13:19 +00:00
|
|
|
http_log
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
|
|
|
|
|
2021-02-04 08:28:41 +00:00
|
|
|
def forbidden(e)
|
|
|
|
|
logger.info { e }
|
|
|
|
|
error = humanize_error(e)
|
|
|
|
|
response.headers['X-Failure'] = error.fetch(:error_human, error[:error])
|
|
|
|
|
respond_to_exception(e, :forbidden)
|
|
|
|
|
http_log
|
|
|
|
|
end
|
|
|
|
|
|
2020-03-19 09:39:51 +00:00
|
|
|
def pundit_not_authorized_error(e)
|
|
|
|
|
logger.info { e }
|
|
|
|
|
# check if a special authorization_error should be shown in the result payload
|
|
|
|
|
# which was raised in one of the policies. Fall back to a simple "Not authorized"
|
|
|
|
|
# error to hide actual cause for security reasons.
|
2022-06-29 06:13:09 +00:00
|
|
|
exception = e.policy&.custom_exception || Exceptions::Forbidden.new(__('Not authorized'))
|
|
|
|
|
|
|
|
|
|
case exception
|
|
|
|
|
when ActiveRecord::RecordNotFound
|
|
|
|
|
not_found(exception)
|
2026-04-22 15:23:21 +00:00
|
|
|
when Exceptions::UnprocessableContent
|
|
|
|
|
unprocessable_content(exception)
|
2022-06-29 06:13:09 +00:00
|
|
|
else
|
|
|
|
|
forbidden(exception)
|
|
|
|
|
end
|
2020-03-19 09:39:51 +00:00
|
|
|
end
|
|
|
|
|
|
2017-01-20 09:45:19 +00:00
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def respond_to_exception(e, status)
|
|
|
|
|
status_code = Rack::Utils.status_code(status)
|
|
|
|
|
|
|
|
|
|
respond_to do |format|
|
2020-03-06 14:31:43 +00:00
|
|
|
format.json { render json: humanize_error(e), status: status }
|
2017-10-01 12:25:52 +00:00
|
|
|
format.any do
|
2020-03-06 14:31:43 +00:00
|
|
|
errors = humanize_error(e)
|
2017-01-20 09:45:19 +00:00
|
|
|
@exception = e
|
2018-07-17 08:35:51 +00:00
|
|
|
@message = errors[:error_human] || errors[:error] || param[:message]
|
2017-01-20 09:45:19 +00:00
|
|
|
@traceback = !Rails.env.production?
|
2025-06-10 07:53:01 +00:00
|
|
|
file = Rails.public_path.join("#{status_code}#{'-mobile' if params[:controller] == 'mobile'}.html").open('r')
|
2021-04-27 08:54:37 +00:00
|
|
|
render inline: file.read, status: status, content_type: 'text/html' # rubocop:disable Rails/RenderInline
|
2017-10-01 12:25:52 +00:00
|
|
|
end
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2020-03-06 14:31:43 +00:00
|
|
|
def humanize_error(e)
|
2023-11-08 09:38:22 +00:00
|
|
|
data = { error: e.message }
|
2017-01-20 09:45:19 +00:00
|
|
|
|
2022-03-04 22:51:19 +00:00
|
|
|
if (base_error = e.try(:record)&.errors&.messages&.find { |key, _| key.match? %r{[\w+.]?base} }&.last&.last)
|
|
|
|
|
data[:error_human] = base_error
|
|
|
|
|
elsif (first_error = e.try(:record)&.errors&.full_messages&.first)
|
|
|
|
|
data[:error_human] = first_error
|
2021-05-12 11:37:44 +00:00
|
|
|
elsif e.message.match?(%r{(already exists|duplicate key|duplicate entry)}i)
|
2022-10-25 13:35:13 +00:00
|
|
|
data[:error_human] = __('This object already exists.')
|
2021-05-12 11:37:44 +00:00
|
|
|
elsif e.message =~ %r{null value in column "(.+?)" violates not-null constraint}i || e.message =~ %r{Field '(.+?)' doesn't have a default value}i
|
2017-01-20 09:45:19 +00:00
|
|
|
data[:error_human] = "Attribute '#{$1}' required!"
|
2021-02-04 08:28:41 +00:00
|
|
|
elsif e.message == 'Exceptions::Forbidden'
|
2021-11-15 15:58:19 +00:00
|
|
|
data[:error] = __('Not authorized')
|
2017-01-20 09:45:19 +00:00
|
|
|
data[:error_human] = data[:error]
|
2021-02-04 08:28:41 +00:00
|
|
|
elsif e.message == 'Exceptions::NotAuthorized'
|
2021-11-15 15:58:19 +00:00
|
|
|
data[:error] = __('Authorization failed')
|
2021-02-04 08:28:41 +00:00
|
|
|
data[:error_human] = data[:error]
|
2025-05-14 09:25:37 +00:00
|
|
|
elsif e.instance_of?(Exceptions::InvalidAttribute)
|
|
|
|
|
data[:invalid_attribute] = { e.attribute => data[:error] }
|
2026-04-22 15:23:21 +00:00
|
|
|
elsif e.is_a?(Exceptions::UnprocessableContent)
|
2025-05-14 16:55:17 +00:00
|
|
|
data[:error_human] = data[:error]
|
2026-04-22 15:23:21 +00:00
|
|
|
data[:unprocessable_content] = e.content
|
2026-02-17 11:54:08 +00:00
|
|
|
elsif e.is_a?(Exceptions::InvalidCSRFToken)
|
|
|
|
|
data[:error_human] = data[:error]
|
|
|
|
|
data[:invalid_csrf_token] = true
|
2025-05-14 16:55:17 +00:00
|
|
|
elsif [ActionController::RoutingError, ActiveRecord::RecordNotFound, Exceptions::NotAuthorized, Exceptions::Forbidden, Store::Provider::S3::Error, Authorization::Provider::AccountError, Exceptions::MissingAttribute, ActionController::ParameterMissing].include?(e.class)
|
2020-03-06 14:31:43 +00:00
|
|
|
data[:error_human] = data[:error]
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
|
|
|
|
|
2020-03-06 14:31:43 +00:00
|
|
|
if data[:error_human].present?
|
|
|
|
|
data[:error] = data[:error_human]
|
2021-07-23 13:07:16 +00:00
|
|
|
elsif !policy(Exceptions).view_details?
|
2020-03-06 14:31:43 +00:00
|
|
|
error_code_prefix = "Error ID #{SecureRandom.urlsafe_base64(6)}:"
|
|
|
|
|
Rails.logger.error "#{error_code_prefix} #{data[:error]}"
|
|
|
|
|
data[:error] = "#{error_code_prefix} Please contact your administrator."
|
2017-01-20 09:45:19 +00:00
|
|
|
end
|
2020-03-06 14:31:43 +00:00
|
|
|
|
2017-01-20 09:45:19 +00:00
|
|
|
data
|
|
|
|
|
end
|
|
|
|
|
end
|