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:
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.
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.