grape icon indicating copy to clipboard operation
grape copied to clipboard

Was it intentional to hide the http response status from the error formatter?

Open drewnichols opened this issue 10 months ago • 14 comments

I'm curious if this was discussed before and done intentionally.

For some reason, the status value is not passed into the format_message method here in error!:

https://github.com/ruby-grape/grape/blob/5ce44def9ce7026ad140a31a146820d081ab6798/lib/grape/middleware/error.rb#L132-L137

Passing it would make including it in the response body JSON easier. This is the way it's included in the JSON API error response bodies:

# HTTP/1.1 422 Unprocessable Entity
# Content-Type: application/vnd.api+json

{
  "errors": [
    {
      "status": "422",
      "source": { "pointer": "/data/attributes/firstName" },
      "title":  "Invalid Attribute",
      "detail": "First name must contain at least two characters."
    }
  ]
}

drewnichols avatar Jan 24 '25 22:01 drewnichols

I don't think this was intentional at all, please feel free to PR a change.

dblock avatar Jan 25 '25 19:01 dblock

I will make it so

drewnichols avatar Jan 27 '25 22:01 drewnichols

Ain't the most intuitive way but you can access the status from the env variable which is Rack's env.

You can access endpoint's status code like this env[Grape::Env::API_ENDPOINT].status

describe 'error formatter env' do
  subject { last_response.body }

  let(:app) do
    Class.new(described_class) do
      format :json

      error_formatter :json, ->(message, _backtrace, _options, env, _original_exception) {
        "#{message}:#{env[Grape::Env::API_ENDPOINT].status}"
      }

      rescue_from :all do
        error!("I'm a teapot!", 418)
      end

      get { raise StandardError }
    end
  end

  before do
    get '/'
  end

  it { is_expected.to eq("I'm a teapot!:418") }
end

@dblock To be more intuitive, I would suggest to revisit the signature to also include headers. It would be a breaking change obviously but for the better good I think.

ericproulx avatar Jan 29 '25 20:01 ericproulx

@drewnichols I think I agree with @ericproulx

  1. Does the workaround of env[Grape::Env::API_ENDPOINT].status work for you?
  2. Maybe we can add a locally scoped status in the error_formatter context?

dblock avatar Jan 30 '25 08:01 dblock

The env[Grape::Env::API_ENDPOINT].status seems to be set in many cases but not all. I found that it works when I have custom rescue_from blocks like the following.

  rescue_from NotAuthenticatedError do |e|
    error!(e.message, 401, {}, e.backtrace, e)
  end

Ironically for grape errors rescued with the following

    rescue_from :grape_exceptions

we get

Grape::Env::API_ENDPOINT].status == 201

I'm sure there is a fix for this but it feels like an indirect long-term solution that could be broken in the future.

drewnichols avatar Jan 30 '25 19:01 drewnichols

So any ideas about making this change backwards compatible?

dblock avatar Jan 31 '25 11:01 dblock

I need another week or so to work on it. Been busy with real job and it's snowing this weekend.

drewnichols avatar Jan 31 '25 18:01 drewnichols

I see the issue now. Anyone (like me) who created a formatter as a module like the following will get ArgumentError: wrong number of arguments (given 6, expected 5).

module CustomFormatter
  def self.call(message, backtrace, options, env, original_exception)
    { message: message, backtrace: backtrace }
  end
end

I'm guessing this is not encouraged but we could get around this by inspecting the CustomFormatter call method and checking the argument count. Something like the following in Grape::Middleware::Error.format_message.

if formatter.method(:call).arity == 6
  return formatter.call(message, backtrace, options, env, original_exception, status)
else
  return formatter.call(message, backtrace, options, env, original_exception)
end

drewnichols avatar Feb 01 '25 00:02 drewnichols

I dislike the arity check a lot, but I am not seeing another option either.

My bigger concern is that this change is not future proof as we might find more things we want to pass along into custom formatters. Am I wrong to think this is a bad interface?

We could step back a little. Some ideas.

  1. Hold this exact change back till a major version increment (all problems above).
  2. Redesign this interface to take options only.
  3. Change the calling context for custom formatters to have access to self.error, self.env, etc.

WDYT?

dblock avatar Feb 01 '25 11:02 dblock

I think we should aim on a function with a signature that looks like this

def call(**args)
 ...
end

It gives a lot of flexibility and more future proof. Grape's internals can pass any parameters without breaking the signature.

Plus, anyone can redefine with explicit keyword args. For instance

def call(message:, status:, backtrace:, **_opts)
...
end

ericproulx avatar Feb 01 '25 13:02 ericproulx

The env[Grape::Env::API_ENDPOINT].status seems to be set in many cases but not all. I found that it works when I have custom rescue_from blocks like the following.

rescue_from NotAuthenticatedError do |e| error!(e.message, 401, {}, e.backtrace, e) end

Ironically for grape errors rescued with the following

rescue_from :grape_exceptions

we get

Grape::Env::API_ENDPOINT].status == 201

I'm sure there is a fix for this but it feels like an indirect long-term solution that could be broken in the future.

I will check that. It supposed to return the appropriate status code

ericproulx avatar Feb 01 '25 14:02 ericproulx

@drewnichols I've a fix coming when using rescue_from without block.

ericproulx avatar Feb 01 '25 14:02 ericproulx

@drewnichols #2530 should fix the issue regarding the endpoint's status when using rescue_from without a block.

ericproulx avatar Feb 01 '25 16:02 ericproulx

#2530 worked I can now access the HTTP status via env[Grape::Env::API_ENDPOINT].status

drewnichols avatar Feb 06 '25 00:02 drewnichols