Обзор Action Controller

По этому руководству вы изучите, как работают контроллеры, и как они вписываются в цикл запроса к вашему приложению.

После его прочтения, вы узнаете, как:

  • Следить за ходом запроса через контроллер.
  • Ограничить параметры, переданные в контроллер.
  • Хранятся данные в сессии или куки, и зачем.
  • Работать с колбэками экшна для выполнения кода в течение обработки запроса.
  • Использовать встроенную в Action Controller HTTP аутентификацию.
  • Направлять потоковые данные прямо в браузер пользователя.
  • Отфильтровывать чувствительные параметры, чтобы они не появлялись в логах приложения.
  • Работать с исключениями, которые могут порождаться в течение обработки запроса.
  • Использовать встроенную конечную точку проверки работоспособности для балансировщиков нагрузки и мониторов работоспособности.

1. Что делает контроллер?

Action Controller это C в аббревиатуре MVC. После того, как роутер определит, какой контроллер использовать для обработки запроса, контроллер ответственен за осмысление запроса и генерацию подходящего ответа. К счастью, Action Controller делает за вас большую часть грязной работы и использует элегантные соглашения, чтобы сделать это по возможности максимально просто.

Для большинства приложений, основанных на RESTful, контроллер получает запрос (это невидимо для вас, как для разработчика), извлекает или сохраняет данные в модели и использует вью для создания результирующего HTML. Если контроллеру необходимо работать немного по-другому, не проблема, это всего лишь наиболее распространенный способ работы контроллера.

Таким образом, контроллер можно рассматривать как посредника между моделями и вью. Он делает данные модели доступными вью, так что она может отображать эти данные пользователю, и он сохраняет или обновляет данные от пользователя в модель.

Более детально о процессе маршрутизации смотрите Роутинг в Rails.

2. Соглашение по именованию контроллеров

Соглашение по именованию контроллеров в Rails устанавливает предпочтение множественного числа в последнем слове имени контроллера, хотя строго это не требуется (например, ApplicationController). К примеру, ClientsController более предпочтителен, чем ClientController, SiteAdminsController более предпочтителен, чем SiteAdminController или SitesAdminsController, и так далее.

Следование этому соглашению позволяет вам использовать генераторы маршрутов по умолчанию (например, resources и т.п.) без необходимости определять каждый :path или :controller, и сохраняет последовательным использование хелперов именованных путей во всем вашем приложении. Подробнее смотрите в руководстве Макеты и рендеринг в Rails.

Соглашение по именованию контроллеров отличается от соглашения по именованию моделей, которые, как ожидается, будут именоваться в единственном числе.

3. Методы и экшны

Контроллер - это класс Ruby, унаследованный от ApplicationController и содержащий методы, как и любой другой класс. Когда ваше приложение получает запрос, роутинг определяет, какой контроллер и экшн нужно запустить, затем Rails создает экземпляр этого контроллера и запускает метод с именем, как у экшна.

class ClientsController < ApplicationController
  def new
  end
end

В качестве примера, если пользователь перейдет на /clients/new в приложении, чтобы добавить нового клиента, Rails создаст экземпляр ClientsController и вызовет метод new. Отметьте, что пустой метод из вышеприведенного примера будет прекрасно работать, так как Rails по умолчанию отрендерит вью new.html.erb, если в экшне не будет указано иное. При создании нового Client, метод new может сделать переменную экземпляра @client доступной во вью:

def new
  @client = Client.new
end

Руководство Макеты и рендеринг в Rails объясняет это более детально.

ApplicationController унаследован от ActionController::Base, который определяет несколько полезных методов. Это руководство раскроет часть из них, но если вы любопытны, можете увидеть их все в документации по API.

Только public методы могут быть вызваны как экшны. Хорошей практикой является уменьшение области видимости методов (при помощи private или protected), не предназначенных быть экшнами, таких как вспомогательные методы и фильтры.

Некоторые имена методов зарезервированы в Controller. Случайное переопределение их как экшны, или даже как вспомогательные методы, может привести к SystemStackError. Если вы ограничиваете свои контроллеры иметь только RESTful экшны в [Ресурсном роутинге][], об этом можно не беспокоиться.

Если вы должны использовать зарезервированные методы в качестве имени экшна, это можно обойти, используя пользовательский маршрут, для направления зарезервированного имени метода на не зарезервированный метод экшна.

4. Параметры

Возможно, вы хотите получить доступ к данным, посланным пользователем, или к другим параметрам в экшнах вашего контроллера. Существует два типа параметров, доступных в веб-приложениях. Первый - это параметры, посланные как часть URL, называемые параметрами строки запроса. Строка запроса всегда следует после "?" в URL. Второй тип параметров обычно упоминается как данные POST. Эта информация обычно приходит из формы HTML, заполняемой пользователем. Эти параметры еще называют данными POST, так как могут быть посланы только как часть HTTP-запроса метода POST. Rails не делает каких-либо различий между строковыми параметрами и параметрами POST, и они оба доступны в хэше params в вашем контроллере:

class ClientsController < ApplicationController
  # Этот экшн использует параметры строки запроса, потому что он
  # запускается HTTP-запросом метода GET, но это не влияет на
  # то, как можно получить доступ к ним.
  # URL для этого экшна выглядит как этот, запрашивающий список
  # активированных клиентов: /clients?status=activated
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end

  # Этот экшн использует параметры POST. Они, скорее всего, пришли от
  # формы HTML, которую подтвердил пользователь. URL для этого
  # RESTful запроса будет "/clients", и данные будут посланы
  # как часть тела запроса.
  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      # Эта строчка переопределяет поведение рендеринга по умолчанию,
      # который отрендерил бы вью "create".
      render "new"
    end
  end
end

4.1. Параметры в хэше и в массиве

Хэш params не ограничен одномерными ключами и значениями. Он может содержать вложенные массивы и хэши. Чтобы послать массив значений, добавьте пустую пару квадратных скобок "[]" к имени ключа:

GET /clients?ids[]=1&ids[]=2&ids[]=3

Фактический URL в этом примере будет перекодирован как "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5b=3", так как "[" и "]" недопустимы в URL. В основном, вам не придется беспокоиться об этом, так как браузер позаботится об этом за вас, а Rails декодирует это обратно, когда получит, но если вы когда-нибудь будете отправлять эти запросы вручную, имейте это в виду.

Значение params[:ids] теперь будет ["1", "2", "3"]. Отметьте, что значения параметра всегда строковое; Rails не пытается угадать или предсказать тип.

Значения, такие как [nil] или [nil, nil, ...] в params по умолчанию заменяются на [] по причине безопасности. Подробнее смотрите в руководстве Безопасность приложений на Rails.

Чтобы послать хэш, следует заключить имя ключа в скобки:

<form accept-charset="UTF-8" action="/clients" method="post">
  <input type="text" name="client[name]" value="Acme" />
  <input type="text" name="client[phone]" value="12345" />
  <input type="text" name="client[address][postcode]" value="12345" />
  <input type="text" name="client[address][city]" value="Carrot City" />
</form>

Когда эта форма будет подтверждена, значение params[:client] будет { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }. Обратите внимание на вложенный хэш в params[:client][:address].

Объект params ведет себя как хэш, но позволяет взаимозаменяемо использовать символы и строки как ключи.

4.2. Параметры JSON

Если ваше приложение предоставляет API, вы, вероятно, будете принимать параметры в формате JSON. Если заголовок "Content-Type" вашего запроса установлен на "application/json", Rails автоматически загрузит ваши параметры в хэш params, к которому вы сможете получить доступ обычным способом.

Так, к примеру, если вы пошлете такое содержимое JSON:

{ "company": { "name": "acme", "address": "123 Carrot Street" } }

Ваш контроллер будет получать params[:company] как { "name" => "acme", "address" => "123 Carrot Street" }.

Также, если включите config.wrap_parameters в своем инициализаторе или вызовете wrap_parameters в своем контроллере, можно безопасно опустить корневой элемент в параметре JSON. Параметры будут клонированы и обернуты в ключ, соответствующий по умолчанию имени вашего контроллера. Таким образом, вышеупомянутый запрос JSON может быть записан как:

{ "name": "acme", "address": "123 Carrot Street" }

И предположим, что мы посылаем данные в CompaniesController, тогда он будет обернут в ключ :company следующим образом:

{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }

Вы сможете настроить имя ключа или определенные параметры, которые вы хотите обернуть, ознакомившись с документацией по API.

Поддержка парсинга параметров XML была извлечена в гем actionpack-xml_parser.

4.3. Параметры роутинга

Хэш params будет всегда содержать ключи :controller и :action, но следует использовать методы controller_name и action_name вместо них для доступа к этим значениям. Любой другой параметр, определенный роутингом, такой как :id, также будет доступен. Например, рассмотрим перечень клиентов, где список может быть показан либо для активных, либо для неактивных клиентов. Мы можем добавить маршрут, перехватывающий параметр :status в "красивом" URL:

get '/clients/:status', to: 'clients#index', foo: 'bar'

В этом случае, когда пользователь откроет URL /clients/active, params[:status] будет установлен в "active". Когда использован этот маршрут, params[:foo] также будет установлен в "bar", как будто он был передан в строке запроса. Ваш контроллер также получит params[:action] как "index" и params[:controller] как "clients".

4.4. Параметры составного ключа

Параметры составного ключа содержат несколько значений в одном параметре. По этой причине нам нужно иметь возможность извлекать каждое значение и передавать их в Active Record. Для этого случая мы можем использовать метод extract_value.

Рассмотрим следующий контроллер:

class BooksController < ApplicationController
  def show
    # Извлекаем значение составного ID из параметров URL.
    id = params.extract_value(:id)
    # Находим книгу по составному ID.
    @book = Book.find(id)
    # Используем поведение отрисовки по умолчанию для отрисовки вью show.
  end
end

И следующий маршрут:

get '/books/:id', to: 'books#show'

Когда пользователь открывает URL /books/4_2, контроллер извлекает составное значение ключа ["4", "2"] и передает его в Book.find для отрисовки нужной записи во вью. Метод extract_value может использоваться для извлечения массивов из любых параметров с разделителями.

4.5. default_url_options

Можно установить глобальные параметры по умолчанию для генерации URL, определив в контроллере метод по имени default_url_options. Этот метод должен возвращать хэш с желаемыми значениями по умолчанию, ключи которого должны быть символами:

class ApplicationController < ActionController::Base
  def default_url_options
    { locale: I18n.locale }
  end
end

Эти опции будут использованы как начальная точка при генерации URL, поэтому они могут быть переопределены опциями, переданными в url_for.

Если определить default_url_options в ApplicationController, как это показано в вышеприведенном примере, эти значения по умолчанию будут использованы для генерации всех URL. Этот метод также может быть определен в одном отдельном контроллере, и в этом случае он влияет только на URL, сгенерированные в нем.

В данном запросе, на самом деле, метод не вызывается для каждого сгенерированного URL; для повышения производительности, возвращаемый хэш кэшируется, и метод выполняется не более одного раза за запрос.

4.6. Strong Parameters

С помощью сильных параметров (strong parameters) параметры Action Controller запрещены к использованию в массовых назначениях Active Model до тех пор, пока они не разрешены. Это означает, что нужно будет принять осознанное решение о том, какие атрибуты будут разрешены для массового обновления. Это лучший способ предотвратить случайную уязвимость, позволяющую пользователям обновлять чувствительные атрибуты модели.

Кроме того, параметры могут быть помечены как обязательные и будут проходить через предопределенные raise/rescue, что приведет к 400 Bad Request, если не будут переданы все обязательные параметры.

class PeopleController < ActionController::Base
  # Это вызовет исключение ActiveModel::ForbiddenAttributesError,
  # так как используется массовое назначение без явного шага permit.
  def create
    Person.create(params[:person])
  end

  # Это будет выполняться должным образом, пока в параметрах есть ключ person, иначе будет
  # вызвано исключение ActionController::ParameterMissing, которое будет
  # поймано в ActionController::Base и превращено в ошибку 400 Bad Request.
  def update
    person = current_account.people.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end

  private
    # Использование приватного метода для инкапсуляции разрешенных параметров -
    # это всего лишь хороший паттерн, с помощью которого можно повторно
    # использовать тот же самый список разрешений при создании и обновлении.
    # Этот метод также можно адаптировать к проверке разрешенных атрибутов для
    # каждого пользователя.
    def person_params
      params.require(:person).permit(:name, :age)
    end
end
4.6.1. Разрешенные скалярные значения

Вызов permit подобный:

params.permit(:id)

разрешает указанный ключ (:id) для включения, если он появится в params и будет иметь разрешенное скалярное значение. В ином случае ключ будет отфильтрован, таким образом, массивы, хэши и любые другие объекты не смогут быть переданы.

Разрешенные скалярные типы следующие String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO, ActionDispatch::Http::UploadedFile и Rack::Test::UploadedFile.

Чтобы объявить, что значение в params должно быть массивом разрешенных скалярных значений, свяжите ключ с пустым массивом:

params.permit(id: [])

Иногда невозможно или неудобно объявлять валидные ключи параметров хэша или его внутреннюю структуру. Просто укажите пустой хэш:

params.permit(preferences: {})

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

Чтобы разрешить полный хэш параметров, можно использовать метод permit!:

params.require(:log_entry).permit!

Это помечает хэш параметров :log_entry и любые вложенные хэши как разрешенные, и не проверяет разрешенные скалярные величины, принимается все. Следует соблюдать предельную осторожность при использовании permit!, так как он позволит массовое назначение всех текущих и будущих атрибутов модели.

4.6.2. Вложенные параметры

Также можно использовать permit c вложенными параметрами, например:

params.permit(:name, { emails: [] },
              friends: [ :name,
                         { family: [ :name ], hobbies: [] }])

Это объявление разрешает атрибуты name, emails и friends. Ожидается, что emails будет массивом разрешенных скалярных значений, и что friends будет массивом ресурсов с определенными атрибутами: у них будет атрибут name (допустимо любое скалярное значение), атрибут hobbies как массив разрешенных скалярных значений, и атрибут family, который может иметь только name (также допустимо любое скалярное значение).

4.6.3. Дополнительные примеры

Возможно вы захотите использовать разрешенные атрибуты в экшне new. В этой связи возникает проблема, из-за которой нельзя использовать require на корневом ключе, так как обычно он не существует при вызове new:

# используя `fetch`, можно предоставить значение по умолчанию и использовать
# далее Strong Parameters API.
params.fetch(:blog, {}).permit(:title, :author)

Метод класса модели accepts_nested_attributes_for позволяет обновлять и удалять связанные записи. Он основывается на параметрах id и _destroy:

# permit :id и :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])

Хэши с числовыми ключами трактуются по-другому, и можно объявить атрибуты так, как будто они являются прямыми детьми. Такой тип параметров можно получить при использовании accepts_nested_attributes_for в сочетании со связью has_many:

# Чтобы разрешить следующие данные:
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}

params.require(:book).permit(:title, chapters_attributes: [:title])

Рассмотрим ситуацию, когда есть параметры, представляющие имя продукта, и хэш произвольных данных, связанных с этим продуктом, и нужно разрешить атрибут name продукта, а также весь хэш данных:

def product_params
  params.require(:product).permit(:name, data: {})
end
4.6.4. За пределами области видимости Strong Parameters

Strong parameter API был разработан для наиболее общих вариантов использования. Это не панацея от всех ваших проблем белого списка. Однако можно легко смешивать API с вашим собственным кодом для адаптации к вашей ситуации.

5. Сессия

В приложении есть сессия для каждого пользователя, в которой можно хранить небольшие объемы данных, которые будут персистентными между запросами. Сессия доступна только в контроллере и во вью, и может использовать один из нескольких механизмов хранения:

  • ActionDispatch::Session::CookieStore - Хранит все на клиенте.
  • ActionDispatch::Session::CacheStore - Хранит данные в кэше Rails.
  • ActionDispatch::Session::MemCacheStore - Хранит данные в кластере memcached (эта устаревшая реализация, вместо нее рассмотрите использование CacheStore).
  • ActionDispatch::Session::ActiveRecordStore - Хранит данные в базе данных с использованием Active Record. (требует гем activerecord-session_store).
  • Пользовательское хранилище или хранилище, предоставленное сторонним гемом

Все хранилища сессии используют куки для хранения уникального ID каждой сессии (вы должны использовать куки, Rails не позволяет передавать ID сессии в URL, так как это менее безопасно).

В большинстве хранилищ этот ID используется для поиска данных сессии на сервере, в т.ч. в таблице базы данных. Имеется одно исключение, это дефолтное и рекомендуемое хранилище сессии - CookieStore - которое хранит все данные сессии в куки (ID остается по-прежнему доступным, если он нужен). Преимущества его заключаются в легкости, отсутствии необходимости настройки для нового приложения чтобы использовать сессию. Данные в куки криптографически подписаны, что делает их защищенными от взлома. И они также зашифрованы, таким образом любой получивший к ним доступ, не сможет прочитать их содержимое (Rails не примет их, если они были отредактированы).

CookieStore могут хранить около 4 Кбайт данных - намного меньше, чем остальные - но этого обычно хватает. Хранение большего количества данных в сессии не рекомендуется, вне зависимости от того, какое хранилище сессии используется в приложении. Особенно следует избегать хранения в сессии сложных объектов (такие как экземпляры модели), так как сервер может не собрать их между запросами, что приведет к ошибке.

Если пользовательские сессии не хранят критически важные данные или нет необходимости в ее сохранности на долгий период (скажем, если она используется только для флеш-сообщений), можно рассмотреть использование ActionDispatch::Session::CacheStore. Он сохранит сессии с использованием реализации кэша, которая была настроена для приложения. Преимущество этого заключается в том, что для хранения сессий можно использовать существующую инфраструктуру кэширования без необходимости дополнительных настроек или администрирования. Недостатком, разумеется, является то, что сессии будут недолговечными и могут исчезнуть в любое время.

Читайте подробнее о хранении сессий в руководстве Безопасность приложений на Rails.

Если вы нуждаетесь в другом механизме хранения сессий, измените его в инициализаторе:

Rails.application.config.session_store :cache_store

Подробности смотрите в config.session_store в руководстве по конфигурации.

Rails устанавливает ключ сессии (имя куки) при подписании данных сессии. Он также может быть изменен в инициализаторе:

# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_your_app_session'

Можете также передать ключ :domain и определить имя домена для куки:

# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"

Rails устанавливает (для CookieStore) секретный ключ, используемый для подписания данных сессии, в config/credentials.yml.enc. Он может быть изменен с помощью bin/rails credentials:edit.

# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 492f...

Изменение secret_key_base при использовании CookieStore делает все предыдущие сессии невалидными.

5.1. Доступ к сессии

В контроллере можно получить доступ к сессии с помощью метода экземпляра session.

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

Значения сессии хранятся, используя пары ключ/значение, подобно хэшу:

class ApplicationController < ActionController::Base

  private

    # Находим пользователя с ID, хранящимся в сессии с ключом
    # :current_user_id Это обычный способ обрабатывать вход пользователя
    # в приложении на Rails; вход устанавливает значение сессии, а
    # выход убирает его.
    def current_user
      @_current_user ||= session[:current_user_id] &&
        User.find_by(id: session[:current_user_id])
    end
end

Чтобы что-то хранить в сессии, просто присвойте это ключу, как в хэше:

class LoginsController < ApplicationController
  # "Создаем" логин (при входе пользователя)
  def create
    if user = User.authenticate(params[:username], params[:password])
      # Сохраняем ID пользователя в сессии, так что он может быть использован
      # в последующих запросах
      session[:current_user_id] = user.id
      redirect_to root_url, status: :see_other root_url
    end
  end
end

Чтобы убрать что-то из сессии, удалите пару ключ/значение:

class LoginsController < ApplicationController
  # "Удаляем" логин (при выходе пользователя)
  def destroy
    # Убираем id пользователя из сессии
    session.delete(:current_user_id)
    # Очистить мемоизированного текущего пользователя
    @_current_user = nil
    redirect_to root_url, status: :see_other
  end
end

Для сброса текущей сессии, используйте reset_session.

5.2. Flash

Flash - это специальная часть сессии, которая очищается с каждым запросом. Это означает, что сохраненные там значения будут доступны только в следующем запросе, что полезно для передачи сообщений об ошибках и т.п.

К флэшу можно получить доступ с помощью метода flash. Подобно сессии, флэш представлен хэшем.

Давайте рассмотрим случай логаута в качестве примера. Контроллер может послать сообщение, которое будет отображено пользователю при следующем запросе:

class LoginsController < ApplicationController
  def destroy
    session.delete(:current_user_id)
    flash[:notice] = "You have successfully logged out."
    redirect_to root_url
  end
end

Отметьте, что также возможно назначить сообщение флэш как часть перенаправления. Можно назначить :notice, :alert или общего назначения :flash:

redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }

Экшн destroy перенаправляет на root_url приложения, где будет отображено сообщение. Отметьте, что от следующего экшна полностью зависит решение, будет ли он или не будет что-то делать с тем, что предыдущий экшн вложил во flash. Принято отображать любые сообщения об ошибке или уведомления из flash в макете приложения:

<html>
  <!-- <head/> -->
  <body>
    <% flash.each do |name, msg| -%>
      <%= content_tag :div, msg, class: name %>
    <% end -%>

    <!-- дальнейшее содержимое -->
  </body>
</html>

В этом случае, если экшн установил сообщения уведомления или предупреждения, макет отобразит их автоматически.

Можно передать все, что только сессия может хранить; вы не ограничены уведомлениями или предупреждениями:

<% if flash[:just_signed_up] %>
  <p class="welcome">Welcome to our site!</p>
<% end %>

Если хотите, чтобы значение flash было перенесено в другой запрос, используйте flash.keep:

class MainController < ApplicationController
  # Давайте скажем этому экшну, соответствующему root_url, что хотим
  # все запросы сюда перенаправить на UsersController#index. Если
  # экшн установил flash и направил сюда, значения в нормальной ситуации
  # будут потеряны, когда произойдет другой редирект, но Вы можете
  # использовать 'keep', чтобы сделать его персистентным для другого запроса.
  def index
    # Все значения flash будут персистентными.
    flash.keep

    # Можете также использовать ключ для сохранения определенных значений.
    # flash.keep(:notice)
    redirect_to users_url
  end
end
5.2.1. flash.now

По умолчанию, добавление значений во flash делает их доступными для следующего запроса, но иногда хочется иметь доступ к этим значениям в том же запросе. Например, если экшн create проваливается при сохранении ресурса, и будет отрендерен непосредственно макет new, что не приведет к новому запросу, но все равно можно отобразить сообщение, используя flash. Чтобы это сделать, используйте flash.now так же, как используете обычный flash:

class ClientsController < ApplicationController
  def create
    @client = Client.new(client_params)
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end

6. Куки

Приложение может хранить небольшое количество данных у клиента - в так называемых куки - которые будут персистентными между запросами и даже сессиями. Rails обеспечивает простой доступ к куки посредством метода cookies, который - очень похож на session - и работает как хэш:

class CommentsController < ApplicationController
  def new
    # Автозаполнение имени комментатора, если оно хранится в куки.
    @comment = Comment.new(author: cookies[:commenter_name])
  end

  def create
    @comment = Comment.new(comment_params)
    if @comment.save
      flash[:notice] = "Thanks for your comment!"
      if params[:remember_name]
        # Запоминаем имя комментатора.
        cookies[:commenter_name] = @comment.author
      else
        # Удаляем из куки имя комментатора, если оно есть.
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

Отметьте, что если для удаления значений сессии можно установить ключ в nil, то для удаления значений куки следует использовать cookies.delete(:key).

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

Эти специальные куки используют сериализатор для сериализации назначенных значений в строки и десериализации их в объекты Ruby при чтении. Можно определить, какой сериализатор использовать с помощью config.action_dispatch.cookies_serializer.

Значением по умолчанию для сериализатора в новых приложениях :json. Следует быть осторожным, потому что JSON обладает ограниченной поддержкой обратной сериализации объектов Ruby. Например, объекты Date, Time и Symbol (включая ключи Hash) будут сериализованы и десериализованы в String:

class CookiesController < ApplicationController
  def set_cookie
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
    redirect_to action: 'read_cookie'
  end

  def read_cookie
    cookies.encrypted[:expiration_date] # => "2014-03-20"
  end
end

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

Если вы храните сессию в куки, все вышесказанное также применяется к хэшам session и flash.

7. Рендеринг

ActionController упрощает рендеринг данных в форматах HTML, XML или JSON. Если сгенерируете контроллер с помощью скаффолда, то он будет выглядеть следующим образом.

class UsersController < ApplicationController
  def index
    @users = User.all
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render xml: @users }
      format.json { render json: @users }
    end
  end
end

Отметьте, что в вышеописанном коде использован render xml: @users, а не render xml: @users.to_xml. Если объект не String, то Rails автоматически вызовет to_xml.

Подробнее о рендеринге можно узнать в Руководстве Макеты и рендеринг в Rails.

8. Колбэки экшна

Колбэки экшна - это методы, которые запускаются "до", "после" или "до и после" экшна контроллера.

Колбэки экшна наследуются, поэтому, если вы установите один в ApplicationController, он будет запущен в каждом контроллере вашего приложения.

Колбэки экшна "before" регистрируются с помощью before_action. Они могут прерывать цикл запроса. Обычный колбэк экшна "before" - это, например, тот, который требует, чтобы пользователь был авторизован для запуска экшна. Метод можно определить следующим образом:

class ApplicationController < ActionController::Base
  before_action :require_login

  private

    def require_login
      unless logged_in?
        flash[:error] = "You must be logged in to access this section"
        redirect_to new_login_url # halts request cycle
      end
    end
end

Метод просто записывает сообщение об ошибке во flash и перенаправляет на форму авторизации, если пользователь не авторизовался. Если колбэк экшна "before" рендерит или перенаправляет, экшн контроллера не запустится. Если есть дополнительные колбэки экшна в очереди, они также будут отменены.

В этом примере колбэк экшна добавлен в ApplicationController, и поэтому все контроллеры в приложении наследуют его. Это приводит к тому, что всё в приложении требует, чтобы пользователь был авторизован, чтобы пользоваться им. По понятным причинам (пользователь не сможет зарегистрироваться в первую очередь!), не все контроллеры или экшны должны требовать его. Вы можете не допустить запуск этого фильтра перед определенными экшнами с помощью skip_before_action:

class LoginsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]
end

Теперь, экшны LoginsController new и create будут работать как раньше, без требования к пользователю быть зарегистрированным. Опция :only используется для пропуска колбэка экшна только для этих экшнов, а также есть опция :except, которая работает наоборот. Эти опции можно использовать и при добавлении колбэков экшна, поэтому необходимо добавить колбэк, который запускается только для выбранных экшнов в первую очередь.

Вызов одного и того же колбэка экшна несколько раз с разными опциями не будет работать, поскольку последнее определение колбэка экшна перезапишет предыдущие.

8.1. Колбэки экшна after и around

В дополнение к колбэкам экшна "before", можно запустить колбэки экшна после того, как экшн контроллера был выполнен, или "и до, и после".

Колбэки экшна "after" регистрируются с помощью after_action. Они похожи на колбэки экшна "before", но поскольку экшн контроллера уже был запущен, у них есть доступ к данным отклика, которые будут отосланы клиенту. Очевидно, колбэки экшна "after" не могут остановить экшн от запуска. Обратите внимание, что колбэки экшна "after" выполняются только после успешного выполнения экшна контроллера, но не при возникновении исключения в цикле запроса.

Колбэки экшна "around" регистрируются с помощью around_action. Они ответственны за запуск связанных с ними экшнов с помощью yield, подобно тому, как работают промежуточные программы Rack.

Например, на веб-сайте, где для изменений есть процедура утверждения информации, администратор может легко их просмотреть, применив их внутри транзакции:

class ChangesController < ApplicationController
  around_action :wrap_in_transaction, only: :show

  private

    def wrap_in_transaction
      ActiveRecord::Base.transaction do
        begin
          yield
        ensure
          raise ActiveRecord::Rollback
        end
      end
    end
end

Отметьте, что колбэки экшна "around" также оборачивают рендеринг. В частности, в вышеуказанном примере, если вью сама начнет считывать из базы данных (например через скоуп), она это осуществит внутри транзакции, предоставив, таким образом, данные для предварительного просмотра.

Можно не вызывать yield и создать отклик самостоятельно, в этом случае экшн контроллера не будет запущен.

8.2. Другие способы использования колбэки экшна

Хотя наиболее распространенный способ использование колбэков экшна - это создание private методов и использование before_action, after_action или around_action для их добавления, есть два других способа делать то же самое.

Первый - это использовать блок прямо в методах *_action. Блок получает контроллер как аргумент. Колбэк экшна require_login может быть переписан с использованием блока:

class ApplicationController < ActionController::Base
  before_action do |controller|
    unless controller.send(:logged_in?)
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url
    end
  end
end

Отметьте, что колбэк экшна в этом случае использует метод send, так как logged_in? является private, и колбэк экшна не запустится в области видимости контроллера. Это не рекомендуемый способ применения такого особого колбэка экшна, но в простых задачах он может быть полезен.

В частности, для around_action в блок также вкладывается action:

around_action { |_controller, action| time(&action) }

Второй способ - это использовать класс (фактически, подойдет любой объект, реагирующий правильными методами) для управления колбэком экшна. Это полезно для более сложных задач, которые не могут быть осуществлены предыдущими двумя способами по причине трудности читаемости и повторного использования. Как пример, можете переписать фильтр авторизации снова, использовав класс:

class ApplicationController < ActionController::Base
  before_action LoginActionCallback
end

class LoginActionCallback
  def self.before(controller)
    unless controller.send(:logged_in?)
      controller.flash[:error] = "You must be logged in to access this section"
      controller.redirect_to controller.new_login_url
    end
  end
end

Опять же, это не идеальный пример для этого колбэка экшна, поскольку он не запускается в области видимости контроллера, а получает контроллер как аргумент. Класс должен реализовывать метод с тем же именем, что и колбэк экшна, поэтому для колбэка экшна before_action класс должен реализовать метод before, и так далее. Метод around должен иметь yield для выполнения экшна.

9. Защита от подделки запроса

Межсайтовая подделка запроса (CSRF, Cross-Site Request Forgery) - это тип атаки, в которой сайт обманом заставляет пользователя сделать запрос на другой сайт, возможно, добавляя, модифицируя или удаляя данные на этом сайте без ведома или прав доступа пользователя.

Первый шаг, чтобы избежать это - убедиться, что все "разрушительные" экшны (создание, обновление и уничтожение) могут быть доступны только не-GET-запросам. Если вы следуете соглашениям RESTful, то уже делаете это. Однако, сайт злоумышленника может также легко послать не-GET-запрос на ваш сайт, поэтому и необходима защита от подделки запроса. Как сказано в названии, он защищает от подделки запроса.

Это можно сделать, добавив неугадываемый токен, известный только вашему серверу, в каждый запрос. При этом способе, если запрос приходит без подходящего токена, ему будет отказано в доступе.

Если вы генерируете подобную форму:

<%= form_with model: @user do |form| %>
  <%= form.text_field :username %>
  <%= form.text_field :password %>
<% end %>

то увидите, как токен будет добавлен в скрытое поле:

<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
       value="67250ab105eb5ad10851c00a5621854a23af5489"
       name="authenticity_token"/>
<!-- fields -->
</form>

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

form_authenticity_token генерирует валидный аутентификационный токен. Его полезно размещать в тех местах, куда Rails не добавляет его автоматически, например в произвольные вызовы Ajax.

В руководстве Безопасность приложений на Rails имеется более подробная информация об этом, и множество других вопросов, посвященных безопасности, которые вы должны принимать во внимание при разработке веб-приложения.

10. Объекты Request и Response

В каждом контроллере есть два акцессор-метода, указывающих на объекты запроса и отклика, связанные с циклом запроса, находящегося в текущее время на стадии выполнения. Метод request содержит экземпляр ActionDispatch::Request, а метод response возвращает объект отклика, представляющий то, что будет отправлено обратно на клиента.

10.1. Объект request

Объект request содержит множество полезной информации о запросе, полученном с клиента. Чтобы получить полный перечень доступных методов, обратитесь к документации по Rails API и документации по Rack. В числе свойств, доступных для этого объекта, следующие:

Свойство request Назначение
host Имя хоста, используемого для этого запроса.
domain(n=2) Первые n сегментов имени хоста, начиная справа (домен верхнего уровня).
format Тип содержимого, запрошенного с клиента.
method Метод HTTP, использованного для запроса.
get?, post?, patch?, put?, delete?, head? Возвращает true, если метод HTTP - это GET/POST/PATCH/PUT/DELETE/HEAD.
headers Возвращает хэш, содержащий заголовки, связанные с запросом.
port Номер порта (целое число), использованного для запроса.
protocol Возвращает строку, содержащую использованный протокол плюс "://", например "http://".
query_string Часть URL со строкой запроса, т.е. все после "?".
remote_ip Адрес IP клиента.
url Полный URL, использованный для запроса.
10.1.1. path_parameters, query_parameters и request_parameters

Rails собирает все параметры, посланные вместе с запросом, в хэше params, были ли они посланы как часть строки запроса, либо в теле запроса post. У объекта request имеется три акцессора, которые предоставляют доступ к этим параметрам в зависимости от того, откуда они пришли. Хэш query_parameters содержит параметры, посланные как часть строки запроса, в то время как хэш request_parameters содержит параметры, посланные как часть тела post. Хэш path_parameters содержит параметры, распознанные роутингом как часть пути, ведущего к определенному контроллеру и экшну.

10.2. Объект response

Объект response (отклик) обычно не используется напрямую, а создается во время выполнения экшна и рендеринга данных, которые посылаются обратно пользователю, но иногда - например, в последующем колбэке экшна - бывает полезно иметь доступ к отклику напрямую. Некоторые из этих акцессор-методов имеют сеттеры, позволяющие изменять их значения. Чтобы получить полный перечень доступных методов, обратитесь к документации по Rails API и документации по Rack.

Свойство response Назначение
body Это строка данных, которая будет возвращена клиенту. Чаще всего это HTML.
status Код статуса HTTP для отклика, например 200 для успешного запроса или 404 для ненайденного файла.
location URL, по которому клиент будет перенаправлен, если указан.
content_type Тип содержимого отклика.
charset Кодировка, используемая для отклика. По умолчанию это "utf-8".
headers Заголовки, используемые для отклика.
10.2.1. Установка пользовательских заголовков

Если хотите установить произвольные заголовки для отклика, то response.headers - как раз то место, что нужно. Атрибут headers - это хэш, который связывает имена заголовков с их значениями, а Rails устанавливает некоторые из них автоматически. Если нужно добавить или изменить заголовок, просто назначьте его response.headers следующим образом:

response.headers["Content-Type"] = "application/pdf"

В вышеприведенном случае более очевидным было бы использование сеттера content_type.

11. Аутентификации HTTP

Rails поставляется с тремя встроенными механизмами аутентификации HTTP:

  • Базовая аутентификация
  • Дайджест-аутентификация
  • Аутентификация по токену

11.1. Базовая аутентификация HTTP

Базовая аутентификация HTTP - это аутентификационная схема, поддерживаемая большинством браузеров и других клиентов HTTP. Как пример, рассмотрим раздел администрирования, который доступен только при вводе имени пользователя и пароля в основном диалоговом окне браузера. Использование встроенной аутентификации требует использования одного метода http_basic_authenticate_with.

class AdminsController < ApplicationController
  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end

Благодаря этому можно создавать именованные контроллеры, наследуемые от AdminsController. Таким образом, предварительный колбэк экшна будет запущен для всех экшнов в этих контроллерах, защищая их с помощью базовой аутентификации HTTP.

11.2. Дайджест-аутентификация HTTP

Дайджест-аутентификация HTTP превосходит базовую аутентификацию, так как она не требует от клиента посылать незашифрованный пароль по сети (хотя базовая аутентификация HTTP безопасна через HTTPS). Чтобы использовать дайджест-аутентификацию с Rails требуется только один метод authenticate_or_request_with_http_digest.

class AdminsController < ApplicationController
  USERS = { "lifo" => "world" }

  before_action :authenticate

  private
    def authenticate
      authenticate_or_request_with_http_digest do |username|
        USERS[username]
      end
    end
end

Как мы видим из примера, блок authenticate_or_request_with_http_digest принимает только один аргумент - имя пользователя. И блок возвращает пароль. Возврат false или nil из authenticate_or_request_with_http_digest вызовет провал аутентификации.

11.3. Аутентификация HTTP по токену

Аутентификация HTTP по токену — это схема для использования токенов Bearer в заголовке HTTP Authorization. Имеется множество доступных форматов токенов, но их описание выходит за рамки этой документации.

В качестве примера, предположим вы хотите использовать аутентификационный токен, выпущенный заранее, для аутентификации и доступа. Реализация аутентификации по токену с помощью Rails требует использования лишь одного метода, authenticate_or_request_with_http_token.

class PostsController < ApplicationController
  TOKEN = "secret"

  before_action :authenticate

  private
    def authenticate
      authenticate_or_request_with_http_token do |token, options|
        ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
      end
    end
end

Как видно из примера, блок authenticate_or_request_with_http_token принимает два аргумента - токен и Hash, содержащий опции, которые были взяты из заголовка HTTP Authorization. Блок должен вернуть true, если аутентификация успешная. Возврат false или nil в нем вызовет неудачу в аутентификации.

12. Потоки и загрузка файлов

Иногда хочется послать пользователю файл вместо рендеринга страницы HTML. Все контроллеры в Rails имеют методы send_data и send_file, которые направляют данные на клиента. send_file - это удобный метод, который позволяет указать имя файла на диске, а он направит содержимое этого файла вам.

Чтобы направить данные на клиента, используйте send_data:

require "prawn"
class ClientsController < ApplicationController
  # Генерирует документ PDF с информацией на клиента и возвращает
  # его. Пользователь получает PDF как загрузку файла.
  def download_pdf
    client = Client.find(params[:id])
    send_data generate_pdf(client),
              filename: "#{client.name}.pdf",
              type: "application/pdf"
  end

  private
    def generate_pdf(client)
      Prawn::Document.new do
        text client.name, align: :center
        text "Address: #{client.address}"
        text "Email: #{client.email}"
      end.render
    end
end

Экшн download_pdf в примере вызовет private метод, который фактически сгенерирует документ PDF и возвратит его как строку. Эта строка будет направлена клиенту как загрузка файла, и пользователю будет предложено имя файла. Иногда при потоковой передаче файлов пользователю может не потребоваться загрузка файла. Возьмите, например, изображения, которые могут быть встроены в страницы HTML. Чтобы сказать браузеру, что файл не предназначен для скачивания, нужно установить опцию :disposition как "inline". Противоположное дефолтное значение этой опции - "attachment".

12.1. Отправка файлов

Если хотите отправить файл, уже существующий на диске, используйте метод send_file.

class ClientsController < ApplicationController
  # Потоковая передача файла, который уже был сгенерирован и сохранен на диск.
  def download_pdf
    client = Client.find(params[:id])
    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
              filename: "#{client.name}.pdf",
              type: "application/pdf")
  end
end

Это прочтет и передаст файл блоками в 4 Кбайт за раз, избегая загрузки в память сразу целого файла. Можно отключить потоковую передачу с помощью опции :stream или отрегулировать размер блока с помощью опции :buffer_size.

Если не указан :type, он будет определяться по расширению файла, указанного в :filename. Если для расширения не зарегистрирован тип содержимого, будет использован application/octet-stream.

Будьте осторожны, когда используете данные, пришедшие с клиента (params, куки и т.д.), для обнаружения файла на диске, так как есть риск безопасности в том, что кто-то может получить доступ к файлам, к которым иметь он не должен.

Не рекомендуется передавать в потоке статичные файлы через Rails, если можно вместо этого разместить их в папке public на веб-сервере. Более эффективно разрешить пользователям скачивать файлы напрямую, используя Apache или другой веб-сервер, сохраняя запрос от ненужного прогона через весь стек Rails.

12.2. Загрузка RESTful

Хотя send_data работает прекрасно, если вы создаете приложение на принципах RESTful, наличие отдельных экшнов для загрузок файла обычно не требуется. В терминологии REST файл PDF из вышеприведенного примера можно рассматривать еще одним представлением ресурса client. Rails предоставляет элегантный способ реализации "RESTful" загрузок. Вот как можно переписать пример, чтобы загрузка PDF была частью экшна show без какой-либо потоковой передачи:

class ClientsController < ApplicationController
  # Пользователь может запросить получение этого ресурса как HTML или PDF.
  def show
    @client = Client.find(params[:id])

    respond_to do |format|
      format.html
      format.pdf { render pdf: generate_pdf(@client) }
    end
  end
end

Чтобы этот пример заработал, нужно добавить PDF тип MIME в Rails. Это можно сделать, добавив следующую строчку в файл config/initializers/mime_types.rb:

Mime::Type.register "application/pdf", :pdf

Конфигурационные файлы не перезагружаются с каждым запросом, поэтому необходимо перезапустить сервер для того, чтобы их изменения вступили в силу.

Теперь пользователь может запрашивать получение версии в PDF, просто добавив ".pdf" в URL:

GET /clients/1.pdf

12.3. Live Streaming произвольных данных

Rails позволяет отдавать в потоке не только файлы. Фактически, в объекте отклика можно отдать все, что хотите. Модуль ActionController::Live позволяет создать персистентное соединение с браузером. Используя этот модуль, можно послать в браузер произвольные данные в определенные моменты времени.

12.3.1. Подключение Live Streaming

Включение ActionController::Live в класс вашего контроллера предоставит всем экшнам контроллера возможность отдавать данные в потоке. Этот модуль можно включить следующим образом:

class MyController < ActionController::Base
  include ActionController::Live

  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "hello world\n"
      sleep 1
    }
  ensure
    response.stream.close
  end
end

Вышеприведенный код будет поддерживать персистентное соединение с браузером и пошлет 100 сообщений "hello world\n", раз в секунду каждое.

В вышеприведенном примере нужно обратить внимание на ряд вещей. Необходимо убедиться, что потоковый отклик будет закрыт. Если забыть закрыть, поток оставит навсегда открытым сокет. Также необходимо установить тип содержимого text/event-stream до записи в поток отклика. Это так, потому что заголовки не могут быть записаны после того, как отклик был совершен (когда response.committed? возвращает истинное значение), которое возникает, когда вызывается write или commit для потокового отклика.

12.3.2. Пример использования

Предположим, мы создаем машину караоке, и пользователь хочет получить слова для определенной песни. В каждом Song имеется определенное количество строчек, и у каждой строчки есть время num_beats для завершения пения.

Если мы хотим возвращать слова по принципу караоке (посылая строчку, только когда певец закончил предыдущую), можно использовать ActionController::Live следующим образом:

class LyricsController < ActionController::Base
  include ActionController::Live

  def show
    response.headers['Content-Type'] = 'text/event-stream'
    song = Song.find(params[:id])

    song.each do |line|
      response.stream.write line.lyrics
      sleep line.num_beats
    end
  ensure
    response.stream.close
  end
end

Вышеприведенный код посылает следующую строчку только после того, как певец завершил предыдущую строчку.

12.3.3. Обсуждение потоковой передачи

Потоковая передача произвольных данных – чрезвычайно мощный инструмент. Как показано в предыдущих примерах, можно выбирать, когда и что посылать в потоковом отклике. Однако, также необходимо отметить следующие вещи:

  • Каждый потоковый отклик создает новый тред и копирует тредовые локальные переменные из текущего треда. Наличие большого количество тредовых локальных переменных может отрицательно сказаться на производительности. Большое количество тредов также препятствует производительности.
  • Незакрытие потокового отклика оставит соответствующий сокет открытым навсегда. Убедитесь, что вызываете close при использовании потокового отклика.
  • Серверы WEBrick буферизируют все отклики, поэтому включение ActionController::Live не будет работать. Необходимо использовать веб-сервер, не буферизирующий отклики автоматически.

13. Фильтрация лога

Rails ведет лог-файл для каждой среды в папке log. Это чрезвычайно полезно при отладке того, что происходит в приложении, но в реальной жизни может быть не нужно хранение каждого бита информации в лог-файле.

13.1. Фильтрация параметров

Можно фильтровать чувствительные параметры запроса в файлах лога, присоединив их к config.filter_parameters в настройках приложения. Эти параметры будут помечены в логе как [FILTERED].

config.filter_parameters << :password

Предоставленные параметры будут отфильтрованы с помощью частично соответствующего регулярного выражения. Rails добавляет список фильтров по умолчанию, включающий :passw, :secret и :token, в соответствующем инициализаторе (initializers/filter_parameter_logging.rb), чтобы обрабатывать типичные параметры приложения, такие как password, password_confirmation и my_token.

13.2. Фильтрация редиректов

Иногда нужно фильтровать из файлов лога некоторые чувствительные места расположения, на которые перенаправляет приложение. Это можно осуществить с использованием конфигурационной опции config.filter_redirect:

config.filter_redirect << 's3.amazonaws.com'

Ей можно передать строку, регулярное выражение или массив из них.

config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]

Соответствующие URL будут помечены как '[FILTERED]'. Впрочем, если вы хотите фильтровать только параметры, а не целые URL-адреса, ознакомьтесь с разделом Фильтрация параметров.

14. Обработка ошибок

Скорее всего, ваше приложение будет содержать программные ошибки или, другими словами, вызывать исключения, которые нужно обработать. Например, если пользователь переходит по ссылке на ресурс, который больше не существует в базе данных, Active Record вызовет исключение ActiveRecord::RecordNotFound.

Дефолтный обработчик исключений Rails отображает сообщение "500 Server Error" для всех исключений. Если запрос сделан локально, отображается прекрасная трассировка и добавляется дополнительная информация, чтобы можно было выяснить, что пошло не так, и разобраться с этим. Если запрос был удаленным, Rails отобразит пользователю лишь простое сообщение "500 Server Error", или "404 Not Found", если была проблема с роутингом или запись не была найдена. Иногда может понадобиться настроить, как эти ошибки будут перехвачены и как они будут отображены пользователю. В приложении на Rails доступны несколько уровней обработки исключений:

14.1. Дефолтные шаблоны 500 и 404

По умолчанию в среде production приложение будет рендерить или 404, или 500 сообщение об ошибке. В среде development будут просто вызываться все необрабатываемые исключения. Эти сообщения содержатся в статичных файлах HTML в папке public, в 404.html и 500.html соответственно. Можно настроить эти файлы, добавив дополнительную информацию и стили, но помните, что они статичные; т.е. нельзя использовать ERB, SCSS, CoffeeScript или макеты для них.

14.2. rescue_from

Если хотите сделать нечто более сложное при перехвате ошибок, можете использовать rescue_from, который обрабатывает исключения определенного типа (или нескольких типов) во всем контроллере и его подклассах.

Когда возникает исключение, которое перехватывается директивой rescue_from, объект исключения передается в обработчик. Обработчик может быть методом или объектом Proc, переданным опции :with. Также можно использовать блок вместо объекта Proc.

Вот как можно использовать rescue_from для перехвата всех ошибок ActiveRecord::RecordNotFound и что-то с ними делать.

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  private
    def record_not_found
      render plain: "404 Not Found", status: 404
    end
end

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

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :user_not_authorized

  private
    def user_not_authorized
      flash[:error] = "You don't have access to this section."
      redirect_back(fallback_location: root_path)
    end
end

class ClientsController < ApplicationController
  # Проверим, что пользователь имеет права авторизации для доступа к клиентам.
  before_action :check_authorization

  # Отметьте, как экшны не беспокоятся об авторизационных делах.
  def edit
    @client = Client.find(params[:id])
  end

  private
    # Если пользователь не авторизован, просто вызываем исключение.
    def check_authorization
      raise User::NotAuthorized unless current_user.admin?
    end
end

Использование rescue_from c Exception или StandardError вызовет серьезные побочные эффекты, поскольку это препятствует Rails правильно обрабатывать исключения. Таким образом, это не рекомендуется делать, если нет для того веской причины.

При запуске в среде running production все ошибки ActiveRecord::RecordNotFound рендерят страницу ошибки 404. Если вам не нужно другое поведение, их не нужно обрабатывать.

Некоторые исключения перехватываемы только из класса ApplicationController, так как они вызываются до того, как контроллер будет инициализирован и экшны будут выполнены.

Если необходимо обеспечить доступ к определенному контроллеру только через HTTPS, нужно сделать это, включив промежуточную программу ActionDispatch::SSL через config.force_ssl в конфигурациях среды.

16. Встроенная конечная точка проверки работоспособности

Rails также поставляется со встроенной конечной точкой проверки работоспособности, доступной по пути /up. Эта конечная точка возвращает код состояния 200, если приложение успешно загрузилось без исключений, и код состояния 500 в противном случае.

В production многим приложениям требуется сообщать свой статус вышестоящим системам, будь то монитор работоспособности, который уведомляет инженеров при возникновении сбоев, или балансировщик нагрузки или контроллер Kubernetes, используемый для определения работоспособности пода. Эта проверка работоспособности является универсальной и подходит для большинства ситуаций.

Хотя все вновь созданные приложения Rails будут иметь проверку работоспособности по пути /up, вы можете настроить этот путь по своему желанию в файле "config/routes.rb":

Rails.application.routes.draw do
  get "healthz" => "rails/health#show", as: :rails_health_check
end

Теперь проверка работоспособности будет доступна по пути /healthz.

эта конечная точка не отражает состояние всех зависимостей вашего приложения, таких как база данных или кластер Redis. Замените "rails/health#show" на свой собственный экшн контроллера, если у вас есть специфические для приложения потребности.

Тщательно продумывайте, что вы хотите проверять, поскольку это может привести к ситуациям, когда ваше приложение перезапускается из-за сбоя стороннего сервиса. В идеале ваше приложение должно быть спроектировано так, чтобы элегантно обрабатывать такие сбои.