You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Rails控制器外部使用rescue_from?——Grape API服务设计模式下的异常处理方案问询

Handling Exceptions Outside Controllers with Grape API & Service Pattern

Great question! The core issue here is that ActiveSupport::Rescuable is built specifically for ActionController contexts—it relies on the controller's request lifecycle to trigger exception handling, which regular service classes don't have. Let's walk through a few practical, DRY solutions to solve this:

Option 1: Build a Custom Rescuable Module for Services

You can replicate the rescue_from behavior manually in a reusable module that works with your service classes. This lets you define exception handlers at the class level and wrap your business logic to catch errors.

First, create a reusable module:

# app/modules/rescuable.rb
module Rescuable
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      @rescue_handlers = {}
    end
  end

  module ClassMethods
    # Define exception handlers (mimics ActiveSupport's rescue_from)
    def rescue_from(exception_class, &block)
      @rescue_handlers[exception_class] = block
    end

    def rescue_handlers
      @rescue_handlers
    end
  end

  # Wrap your business logic to catch and handle exceptions
  def with_rescue
    yield
  rescue => e
    # Find the first matching handler for the exception type
    handler = self.class.rescue_handlers.find do |exception_class, _|
      e.is_a?(exception_class)
    end

    if handler
      handler.last.call(e) # Execute the handler block
    else
      raise e # Re-raise if no handler matches (let upstream handle it)
    end
  end
end

Then use it in your service:

# app/services/exception_handler_service.rb
class ExceptionHandlerService
  include Rescuable

  # Define your exception handlers
  rescue_from ActiveRecord::RecordNotFound do |e|
    { success: false, error: "Record not found", details: e.message }
  end

  rescue_from CustomServiceError do |e|
    { success: false, error: "Service operation failed", details: e.details }
  end

  def perform(some_input)
    with_rescue do
      # Your business logic here—may throw exceptions
      record = User.find(some_input)
      raise CustomServiceError.new("Inactive user") if record.inactive?
      { success: true, data: record }
    end
  end
end

Option 2: Use Grape's Global Exception Handling

Since you're using Grape API, you can leverage Grape's built-in global rescue mechanism to handle exceptions thrown by your services. This keeps your services focused on business logic (throwing meaningful exceptions) and lets the API layer handle translating exceptions to HTTP responses.

First, define custom business exceptions (semantic and easy to target):

# app/exceptions/custom_service_error.rb
class CustomServiceError < StandardError
  attr_reader :details

  def initialize(details)
    @details = details
    super("Service error: #{details}")
  end
end

Then configure your Grape API to rescue these exceptions globally:

# app/api/my_api.rb
class MyAPI < Grape::API
  format :json

  # Global exception handlers
  rescue_from ActiveRecord::RecordNotFound do |e|
    error!({ error: "Not Found", message: e.message }, 404)
  end

  rescue_from CustomServiceError do |e|
    error!({ error: "Service Failure", details: e.details }, 500)
  end

  # Example endpoint using your service
  get "/users/:id" do
    result = UserService.new(params[:id]).perform
    present result
  end
end

In your service, just throw exceptions—no need for per-method rescues:

class UserService
  def perform(user_id)
    user = User.find(user_id)
    raise CustomServiceError.new("User is inactive") if user.inactive?
    user
  end
end

Why Your Original Approach Didn't Work

ActiveSupport::Rescuable's rescue_from method is tied to ActionController's request processing flow. When you include/extend it in a service class, there's no underlying lifecycle hook (like a controller's process_action) to trigger the exception handling. That's why you got the undefined method rescue_from error with extend—the module expects to be included in a class that has the controller-specific infrastructure.

Key Notes

  • Use custom exceptions to make your error handling more semantic and avoid catching overly broad exceptions (like Exception).
  • Choose Option 1 if you need to handle exceptions within the service (e.g., convert errors to success/failure hash responses).
  • Choose Option 2 if you want to centralize HTTP response handling in your Grape API layer (cleaner separation of concerns).

内容的提问来源于stack exchange,提问作者Slim yaw

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 09:47:28