Как заставить Grape возвращать сообщения об ошибках в формате CSV?

У меня есть приложение Rails, и я реализовал API, используя гем Grape. Теперь я создал собственный модуль форматирования ошибок (CSVFormatter), чтобы возвращать ответ об ошибке в формате CSV.

А также у меня есть это в файле v2.rb моего приложения:

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

Когда я нажимаю на такой URL:

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

Он показывает такую ​​ошибку в консоли, что хорошо и означает, что мой пользовательский форматировщик ошибок работает правильно:

Error 
trim_start is invalid 
trim_end is invalid

Но мне просто нужно загрузить это сообщение об ошибке в файле csv. Посмотрев документацию Grape, я нашел способ установить Content-type и попробовал это:

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

Но это не работает, как я ожидал.

ИЗМЕНИТЬ:

Похоже, что нет чистого способа сделать это с помощью винограда без принудительного переопределения кода состояния в соответствии с ответом Саймона. Но это может быть нежелательно, так как это может привести к другим проблемам в приложении, например, если какая-то другая программа попытается прочитать данные из API и получит неверный ответ или что-то подобное, даже не зная почему.


person K M Rakibul Islam    schedule 26.06.2014    source источник


Ответы (1)


Вы ищете заголовок Content-Disposition. Включите это в свой ответ следующим образом:

Content-Disposition: attachment; filename=error.csv

И веб-браузер будет рассматривать тело ответа как файл, который нужно загрузить (в этом примере «error.csv»).

Однако изменение вашего кода для этого осложняется двумя вещами:

  • Из исходного кода Grape становится очевидным, что нет возможности установить заголовки ответа из средства форматирования ошибок, поэтому вам Нам потребуется добавить пользовательский обработчик исключений, который форматирует тело ответа и устанавливает заголовки ответа соответствующим образом для каждого выходного формата, который вы планируете поддерживать.

  • Согласно моим экспериментам, браузеры будут игнорировать заголовок Content-Disposition, если код состояния HTTP указывает на ошибку (например, в диапазоне 400 или 500), поэтому код состояния также необходимо будет переопределить, когда пользователь запрашивает файл CSV.

Попробуйте добавить это в свой класс API:

# 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

Теперь, если вы настроите свой класс API для обработки двух разных форматов (здесь для простоты я выбрал CSV и обычный текст), вот так:

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

Вы должны обнаружить, что ваш API всегда возвращает ответ об ошибке, подходящий для запрошенного формата, и запускает браузер для загрузки ответа только тогда, когда запрашивается формат CSV. Естественно, это может быть расширено для поддержки любого количества форматов путем явного объявления типов контента и средств форматирования ошибок.

Обратите внимание, что есть один случай, когда этот код не выполняет автоматически правильную работу, и это когда ответ об ошибке вызывается напрямую с помощью error!. В этом случае вам нужно будет указать правильное тело и заголовки как часть самого вызова. Я оставлю извлечение соответствующих частей приведенного выше кода в повторно используемые методы в качестве упражнения для читателя.

person Community    schedule 26.06.2014
comment
Мой ответ неверен; Я постоянно забываю, что класс API никогда не создается, поэтому все его методы должны быть статическими. Но сейчас я пробую это на себе, и rescue_from ведет себя не так, как я думал. Вскоре я обновлю свой ответ решением. - person ; 26.06.2014
comment
Верно. Ваша стратегия верна, за исключением того, что, глядя на исходный код Grape, я вижу, что нет способа установить заголовки ответов внутри средства форматирования ошибок. Таким образом, простое решение, если оно существует, должно включать в себя внедрение заголовка в какой-то другой точке на пути между генерируемым исключением и отправкой ответа об ошибке обратно клиенту. - person ; 26.06.2014
comment
Я обновил свой ответ - этот код я проверил сам, и он отлично работает локально. Код не требует пояснений, но если у вас есть вопросы по этому поводу, напишите здесь, и я отвечу. - person ; 26.06.2014