handles_errors.rb 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. module ApplicationController::HandlesErrors
  2. extend ActiveSupport::Concern
  3. included do
  4. rescue_from StandardError, with: :internal_server_error
  5. rescue_from ExecJS::RuntimeError, with: :internal_server_error
  6. rescue_from ActiveRecord::RecordNotFound, with: :not_found
  7. rescue_from ActiveRecord::StatementInvalid, with: :unprocessable_entity
  8. rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
  9. rescue_from ActiveRecord::DeleteRestrictionError, with: :unprocessable_entity
  10. rescue_from ArgumentError, with: :unprocessable_entity
  11. rescue_from Exceptions::UnprocessableEntity, with: :unprocessable_entity
  12. rescue_from Exceptions::NotAuthorized, with: :unauthorized
  13. rescue_from Exceptions::Forbidden, with: :forbidden
  14. rescue_from Pundit::NotAuthorizedError, with: :pundit_not_authorized_error
  15. end
  16. def not_found(e)
  17. logger.error e
  18. respond_to_exception(e, :not_found)
  19. http_log
  20. end
  21. def unprocessable_entity(e)
  22. logger.error e
  23. respond_to_exception(e, :unprocessable_entity)
  24. http_log
  25. end
  26. def internal_server_error(e)
  27. logger.error e
  28. respond_to_exception(e, :internal_server_error)
  29. http_log
  30. end
  31. def unauthorized(e)
  32. logger.info { e }
  33. error = humanize_error(e)
  34. response.headers['X-Failure'] = error.fetch(:error_human, error[:error])
  35. respond_to_exception(e, :unauthorized)
  36. http_log
  37. end
  38. def forbidden(e)
  39. logger.info { e }
  40. error = humanize_error(e)
  41. response.headers['X-Failure'] = error.fetch(:error_human, error[:error])
  42. respond_to_exception(e, :forbidden)
  43. http_log
  44. end
  45. def pundit_not_authorized_error(e)
  46. logger.info { e }
  47. # check if a special authorization_error should be shown in the result payload
  48. # which was raised in one of the policies. Fall back to a simple "Not authorized"
  49. # error to hide actual cause for security reasons.
  50. exeption = e.policy&.custom_exception || Exceptions::Forbidden.new('Not authorized')
  51. forbidden(exeption)
  52. end
  53. private
  54. def respond_to_exception(e, status)
  55. status_code = Rack::Utils.status_code(status)
  56. respond_to do |format|
  57. format.json { render json: humanize_error(e), status: status }
  58. format.any do
  59. errors = humanize_error(e)
  60. @exception = e
  61. @message = errors[:error_human] || errors[:error] || param[:message]
  62. @traceback = !Rails.env.production?
  63. file = File.open(Rails.root.join('public', "#{status_code}.html"), 'r')
  64. render inline: file.read, status: status # rubocop:disable Rails/RenderInline
  65. end
  66. end
  67. end
  68. def humanize_error(e)
  69. data = {
  70. error: e.message
  71. }
  72. if e.message =~ %r{Validation failed: (.+?)(,|$)}i
  73. data[:error_human] = $1
  74. elsif e.message.match?(%r{(already exists|duplicate key|duplicate entry)}i)
  75. data[:error_human] = 'Object already exists!'
  76. elsif e.message =~ %r{null value in column "(.+?)" violates not-null constraint}i || e.message =~ %r{Field '(.+?)' doesn't have a default value}i
  77. data[:error_human] = "Attribute '#{$1}' required!"
  78. elsif e.message == 'Exceptions::Forbidden'
  79. data[:error] = 'Not authorized'
  80. data[:error_human] = data[:error]
  81. elsif e.message == 'Exceptions::NotAuthorized'
  82. data[:error] = 'Authorization failed'
  83. data[:error_human] = data[:error]
  84. elsif [ActionController::RoutingError, ActiveRecord::RecordNotFound, Exceptions::UnprocessableEntity, Exceptions::NotAuthorized, Exceptions::Forbidden].include?(e.class)
  85. data[:error_human] = data[:error]
  86. end
  87. if data[:error_human].present?
  88. data[:error] = data[:error_human]
  89. elsif !current_user&.permissions?('admin')
  90. # We want to avoid leaking of internal information but also want the user
  91. # to give the administrator a reference to find the cause of the error.
  92. # Therefore we generate a one time unique error ID that can be used to
  93. # search the logs and find the actual error message.
  94. error_code_prefix = "Error ID #{SecureRandom.urlsafe_base64(6)}:"
  95. Rails.logger.error "#{error_code_prefix} #{data[:error]}"
  96. data[:error] = "#{error_code_prefix} Please contact your administrator."
  97. end
  98. data
  99. end
  100. end