1

I have a Rails app and I have implemented api using Grape gem. Now, I created a custom error formatter (CSVFormatter) to return error response in CSV format.

And, also I have this in my application's v2.rb file:

error_formatter :csv, Api::Base::Errors::CSVFormatter

When I hit a url like this:

http://example.com/api/v2/datasets/CODE/data.csv?&trim_start=06/01/99&trim_end=2014-05/28&sort_order=desc

It shows the error in the console like this which is good and means that my custom error formatter is working properly:

Error 
trim_start is invalid 
trim_end is invalid

But, I just need to download this error message in a csv file. After looking at Grape's documentation, I found a way of setting Content-type and I tried this:

      rack = Rack::Response.new(as_csv , 422, { "Content-type" => "text/csv" }).finish
      rack[2].body[0]

But, this is not working as I expected.

EDIT:

Looks like there is no clean way of doing it using grape without forcefully overriding the status code according to the answer of Simon. But, one may not wish to do that as it may result other issues in the application like if some other program tries to read the data from the api and gets the incorrect response or so even without knowing why.

K M Rakibul Islam
  • 33,760
  • 12
  • 89
  • 110

1 Answers1

2

You're looking for the Content-Disposition header. Include it in your response like this:

Content-Disposition: attachment; filename=error.csv

And the Web browser will treat the response body as a file to be downloaded (to "error.csv", in this example).

However, modifying your code to do this is complicated by two things:

  • From the Grape source code it's apparent there's no way to set response headers from within an error formatter, so you'll need to add a custom exception handler that formats the response body and sets the response headers appropriately for each output format you plan to support.

  • According to my experimentation, browsers will ignore the Content-Disposition header if the HTTP status code indicates an error (e.g. anything in the 400 or 500 range), so the status code will also need to be overridden when the user requests a CSV file.

Try adding this to your API class:

# Handle all exceptions with an error response appropriate to the requested
# output format
rescue_from :all do |e|
  # Edit this hash to override the HTTP response status for specific output
  # formats
  FORMAT_SPECIFIC_STATUS = {
    :csv => 200
  }

  # Edit this hash to add custom headers specific to each output format
  FORMAT_SPECIFIC_HEADERS = {
    :csv => {
      'Content-Disposition' => 'attachment; filename=error.csv'
    }
  }

  # Get the output format requested by the user
  format = env['api.format']

  # Set the HTTP status appropriately for the requested output format and
  # the error type
  status = FORMAT_SPECIFIC_STATUS[format] ||
             (e.respond_to? :status) && e.status ||
             500

  # Set the HTTP headers appropriately for the requested format
  headers = {
    'Content-Type' => options[:content_types][format] || 'text/plain'
  }.merge(FORMAT_SPECIFIC_HEADERS[format] || { })

  # Format the message body using the appropriate error formatter
  error_formatter =
    options[:error_formatters][format] || options[:default_error_formatter]
  body = error_formatter.call(e.message, nil, options, env)

  # Return the error response to the client in the correct format
  # with the correct HTTP headers for that format
  Rack::Response.new(body, status, headers).finish
end

Now if you configure your API class to handle two different formats (I've picked CSV and plain-text here for simplicity), like this:

module Errors
  module CSVErrorFormatter
    def self.call(message, backtrace, options, env)
      as_csv = "CSV formatter:" + "\n"
      message.split(",").each do |msg|
        as_csv += msg + "\n"
      end

      # Note this method simply returns the response body
      as_csv
    end
  end

  module TextErrorFormatter
    def self.call(message, backtrace, options, env)
      as_txt = "Text formatter:" + "\n"
      message.split(",").each do |msg|
        as_txt += msg + "\n"
      end

      as_txt
    end
  end
end

content_type :csv, 'text/csv'
content_type :txt, 'text/plain'

error_formatter :csv, Api::Base::Errors::CSVErrorFormatter
error_formatter :txt, Api::Base::Errors::TextErrorFormatter

You should find your API always returns an error response suitable for the requested format, and triggers the browser to download the response only when CSV format is requested. Naturally this can be extended to support as many formats as you like, by explicitly declaring content types and error formatters.

Note there's one case in which this code doesn't automatically do the right thing, and that's when an error response is invoked directly using error!. In that case you'll have to supply the correct body and headers as part of the call itself. I'll leave extracting the relevant parts of the above code into reusable methods as an exercise for the reader.

Community
  • 1
  • 1
  • My answer is incorrect; I keep forgetting the API class is never instantiated and so its methods must all be static. But I'm trying this out for myself now and `rescue_from` is not behaving the way I believed it did. I'll update my answer shortly with a solution. –  Jun 26 '14 at 16:16
  • Right. Your strategy is correct, except looking at the Grape source code I can see there is no way to set response headers inside an error formatter. So the easy solution, if there is one, has to involve injecting a header at some other point along the path between an exception being generated and the error response being sent back to the client. –  Jun 26 '14 at 16:40
  • I've updated my answer—this code I've tested myself and it works fine locally. The code is fairly self-explanatory but if you have questions about it, post here and I'll answer. –  Jun 26 '14 at 18:01