1 Introduction
This guide covers a number of advanced topics related to controllers in a Rails application. Please see the Action Controller Overview guide for an introduction to Action Controllers.
2 Authenticity Token and Request Forgery Protection
Cross-site request forgery (CSRF) is a type of malicious attack where unauthorized requests are submitted by impersonating a user that the web application trusts.
The first step to avoid this type of attack is to ensure that all "destructive" actions (create, update, and destroy) in your application use non-GET requests (like POST, PUT and DELETE).
However, a malicious site can still send a non-GET request to your site, so Rails builds in request forgery protection into controllers by default.
This is done by adding a token using the protect_from_forgery method. This token is added to each request and is only known to your server. Rails verifies the received token with the token in the session. If an incoming request does not have the proper matching token, the server will deny access.
The CSRF token is added automatically when config.action_controller.default_protect_from_forgery
is set to true
, which is the default for newly created Rails applications. It can also be manually like this:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
All subclasses of ActionController::Base
are protected by default and
will raise an ActionController::InvalidAuthenticityToken
error on unverified
requests.
2.1 Authenticity Token in Forms
When you generate a form using form_with
like this:
<%= form_with model: @user do |form| %>
<%= form.text_field :username %>
<%= form.text_field :password %>
<% end %>
A CSRF token named authenticity_token
is automatically added as a hidden field
in the generated HTML:
<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
value="67250ab105eb5ad10851c00a5621854a23af5489"
name="authenticity_token"/>
<!-- fields -->
</form>
Rails adds this token to every form
that's generated using the form
helpers, so most of the time you don't need to do anything.
If you're writing a form manually or need to add the token for another reason,
it's available through the
form_authenticity_token
method.
<!-- app/views/layouts/application.html.erb -->
<head>
<meta name="csrf-token" content="<%= form_authenticity_token %>">
</head>
The form_authenticity_token
method generates a valid authentication token.
That can be useful in places where Rails does not add it automatically, like in
custom Ajax calls.
You can learn more details about the CSRF attack as well as CSRF countermeasures in the Security Guide.
3 Controlling Allowed Browser Versions
Starting with version 8.0, Rails controllers use allow_browser
method in ApplicationController
to allow only modern browsers by default.
class ApplicationController < ActionController::Base
# Only allow modern browsers supporting webp images, web push, badges, import # maps, CSS nesting, and CSS :has.
allow_browser versions: :modern
end
Modern browsers includes Safari 17.2+, Chrome 120+, Firefox 121+, Opera 106+. You can use caniuse.com to check for browser versions supporting the features you'd like to use.
In addition to the default of :modern
, you can also specify the browser versions manually:
class ApplicationController < ActionController::Base
# All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+.
allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
end
Browsers matched in the hash passed to versions:
will be blocked if they’re below the versions specified. This means that all other browsers not mentioned in versions:
(Chrome and Opera in the above example), as well as agents that aren’t reporting a user-agent header, will be allowed access.
You can also use allow_browser
in a given controller and specify actions using only
or except
. For example:
class MessagesController < ApplicationController
# In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action.
allow_browser versions: { opera: 104, chrome: 119 }, only: :show
end
A browser that’s blocked will, by default, be served the file in public/406-unsupported-browser.html
with a HTTP status code of “406 Not Acceptable”.
4 HTTP Authentication
Rails comes with three built-in HTTP authentication mechanisms:
- Basic Authentication
- Digest Authentication
- Token Authentication
4.1 HTTP Basic Authentication
HTTP Basic Authentication is a simple authentication method where a user is required to enter a username and password to access a website or a particular section of a website (e.g. admin section). These credentials are entered into a browser's HTTP basic dialog window. The user’s credentials are then encoded and sent in the HTTP header with each request.
HTTP basic authentication is an authentication scheme that is supported by most
browsers. Using HTTP Basic authentication in a Rails controller can be done by
using the http_basic_authenticate_with
method:
class AdminsController < ApplicationController
http_basic_authenticate_with name: "Arthur", password: "42424242"
end
With the above in place, you can create controllers that inherit from
AdminsController
. All actions in those controllers will use HTTP basic
authentication and require user credentials.
HTTP Basic Authentication is easy to implement but not secure on its own, as it will send unencrypted credentials over the network. Make sure to use HTTPS when using Basic Authentication. You can also force HTTPS.
4.2 HTTP Digest Authentication
HTTP digest authentication is more secure than basic authentication as it does not require the client to send an unencrypted password over the network. The credentials are hashed instead and a Digest is sent.
Using digest authentication with Rails can be done by using
the authenticate_or_request_with_http_digest
method:
class AdminsController < ApplicationController
USERS = { "admin" => "helloworld" }
before_action :authenticate
private
def authenticate
authenticate_or_request_with_http_digest do |username|
USERS[username]
end
end
end
The authenticate_or_request_with_http_digest
block takes only one argument -
the username. The block returns the password if found. If the return value is
false
or nil
, it is considered an authentication failure.
4.3 HTTP Token Authentication
Token authentication (aka "Bearer" authentication) is an authentication method
where a client receives a unique token after successfully logging in, which it
then includes in the Authorization
header of future requests. Instead of
sending credentials with each request, the client sends this
token
(a string that represents the user's session) as a "bearer" of the
authentication.
This approach improves security by separating credentials from the ongoing session. You use an authentication token that has been issued in advance to perform authentication.
Implementing token authentication with Rails can be done using the
authenticate_or_request_with_http_token
method.
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
The authenticate_or_request_with_http_token
block takes two arguments - the
token and a hash containing the options that were parsed from the HTTP
Authorization
header. The block should return true
if the authentication is
successful. Returning false
or nil
will cause an authentication failure.
5 Streaming and File Downloads
Rails controllers provide a way to send a file to the user instead of rendering
an HTML page. This can be done with the send_data
and the send_file
methods, which stream data to the client. The send_file
method is a
convenience method that lets you provide the name of a file, and it will stream
the contents of that file.
Here is an example of how to use send_data
:
require "prawn"
class ClientsController < ApplicationController
# Generates a PDF document with information on the client and
# returns it. The user will get the PDF as a file download.
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
The download_pdf
action in the above example calls a private method which
generates the PDF document and returns it as a string. This string will then be
streamed to the client as a file download.
Sometimes when streaming files to the user, you may not want them to download
the file. Take images, for example, which can be embedded into HTML pages. To
tell the browser a file is not meant to be downloaded, you can set the
:disposition
option to "inline". The default value for this option is
"attachment".
5.1 Sending Files
If you want to send a file that already exists on disk, use the send_file
method.
class ClientsController < ApplicationController
# Stream a file that has already been generated and stored on disk.
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
The file will be read and streamed at 4 kB at a time by default, to avoid loading
the entire file into memory at once. You can turn off streaming with the
:stream
option or adjust the block size with the :buffer_size
option.
If :type
is not specified, it will be guessed from the file extension
specified in :filename
. If the content-type is not registered for the
extension, application/octet-stream
will be used.
Be careful when using data coming from the client (params, cookies, etc.) to locate the file on disk. This is a security risk as it might allow someone to gain access to sensitive files.
It is not recommended that you stream static files through Rails if you can instead keep them in a public folder on your web server. It is much more efficient to let the user download the file directly using Apache or another web server, keeping the request from unnecessarily going through the whole Rails stack.
5.2 RESTful Downloads
While send_data
works fine, if you are creating a RESTful application having
separate actions for file downloads is usually not necessary. In REST
terminology, the PDF file from the example above can be considered just another
representation of the client resource. Rails provides a slick way of doing
"RESTful" downloads. Here's how you can rewrite the example so that the PDF
download is a part of the show
action, without any streaming:
class ClientsController < ApplicationController
# The user can request to receive this resource as HTML or PDF.
def show
@client = Client.find(params[:id])
respond_to do |format|
format.html
format.pdf { render pdf: generate_pdf(@client) }
end
end
end
Now the user can request to get a PDF version of a client just by adding ".pdf" to the URL:
GET /clients/1.pdf
You can call any method on format
that is an extension registered as a MIME
type by Rails. Rails already registers common MIME types like "text/html"
and
"application/pdf"
:
Mime::Type.lookup_by_extension(:pdf)
# => "application/pdf"
If you need additional MIME types, call
Mime::Type.register
in the file config/initializers/mime_types.rb
. For example, this is how you
would register the Rich Text Format (RTF):
Mime::Type.register("application/rtf", :rtf)
If you modify an initializer file, you have to restart the server for their changes to take effect.
5.3 Live Streaming of Arbitrary Data
Rails allows you to stream more than just files. In fact, you can stream
anything you would like in a response object. The
ActionController::Live
module allows you to create a persistent connection with a browser. By including
this module in your controller, you can send arbitrary data to the browser at
specific points in time.
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
The above example will keep a persistent connection with the browser and send
100 messages of "hello world\n"
, each one second apart.
Note that you have to make sure to close the response stream, otherwise the
stream will leave the socket open indefinitely. You also have to set the content
type to text/event-stream
before calling write
on the response stream.
Headers cannot be written after the response has been committed (when
response.committed?
returns a truthy value) with either write
or commit
.
5.3.1 Example Use Case
Let's suppose that you were making a karaoke machine, and a user wants to get
the lyrics for a particular song. Each Song
has a particular number of lines
and each line takes time num_beats
to finish singing.
If we wanted to return the lyrics in karaoke fashion (only sending the line when
the singer has finished the previous line), then we could use
ActionController::Live
as follows:
class LyricsController < ActionController::Base
include ActionController::Live
def show
response.headers["Content-Type"] = "text/event-stream"
response.headers["Cache-Control"] = "no-cache"
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
5.3.2 Streaming Considerations
Streaming arbitrary data is an extremely powerful tool. As shown in the previous examples, you can choose when and what to send across a response stream. However, you should also note the following things:
- Each response stream creates a new thread and copies over the thread local variables from the original thread. Having too many thread local variables can negatively impact performance. Similarly, a large number of threads can also hinder performance.
- Failing to close the response stream will leave the corresponding socket open
forever. Make sure to call
close
whenever you are using a response stream. - WEBrick servers buffer all responses, and so streaming with
ActionController::Live
will not work. You must use a web server which does not automatically buffer responses.
6 Log Filtering
Rails keeps a log file for each environment in the log
folder at the
application's root directory. Log files are extremely useful when debugging your
application, but in a production environment you may not want every bit of
information stored in log files. Rails allows you to specify parameters that
should not be stored.
6.1 Parameters Filtering
You can filter out sensitive request parameters from your log files by appending
them to config.filter_parameters
in the application configuration.
config.filter_parameters << :password
These parameters will be marked [FILTERED]
in the log.
The parameters specified in filter_parameters
will be filtered out with
partial matching regular expression. So for example, :passw
will filter out
password
, password_confirmation
, etc.
Rails adds a list of default filters, including :passw
, :secret
, and
:token
, in the appropriate initializer
(initializers/filter_parameter_logging.rb
) to handle typical application
parameters like password
, password_confirmation
and my_token
.
6.2 Redirects Filtering
Sometimes it's desirable to filter out sensitive locations that your application
is redirecting to. You can do that by using the config.filter_redirect
configuration option:
config.filter_redirect << "s3.amazonaws.com"
You can set it to a String, a Regexp, or an Array of both.
config.filter_redirect.concat ["s3.amazonaws.com", /private_path/]
Matching URLs will be replaced with [FILTERED]
. However, if you only wish to
filter the parameters, not the whole URLs, you can use parameter filtering.
7 Force HTTPS Protocol
If you'd like to ensure that communication to your controller is only possible
via HTTPS, you can do so by enabling the ActionDispatch::SSL
middleware
via config.force_ssl
in your environment configuration.
8 Built-in Health Check Endpoint
Rails comes with a built-in health check endpoint that is reachable at the /up
path. This endpoint will return a 200 status code if the app has booted with no
exceptions, and a 500 status code otherwise.
In production, many applications are required to report their status, whether it's to an uptime monitor that will page an engineer when things go wrong, or a load balancer or Kubernetes controller used to determine the health of a given instance. This health check is designed to be a one-size fits all that will work in many situations.
While any newly generated Rails applications will have the health check at
/up
, you can configure the path to be anything you'd like in your
"config/routes.rb":
Rails.application.routes.draw do
get "health" => "rails/health#show", as: :rails_health_check
end
The health check will now be accessible via GET
or HEAD
requests to the
/health
path.
This endpoint does not reflect the status of all of your application's dependencies, such as the database or redis. Replace "rails/health#show" with your own controller action if you have application specific needs.
Reporting the health of an application requires some considerations. You'll have to decide what you want to include in the check. For example, if a third-party service is down and your application reports that it's down due to the dependency, your application may be restarted unnecessarily. Ideally, your application should handle third-party outages gracefully.
9 Handling Errors
Your application will likely contain bugs and throw exceptions that needs to be
handled. For example, if the user follows a link to a resource that no longer
exists in the database, Active Record will throw the
ActiveRecord::RecordNotFound
exception.
Rails default exception handling displays a "500 Server Error" message for all exceptions. If the request was made in development, a nice backtrace and additional information is displayed, to help you figure out what went wrong. If the request was made in production, Rails will display a simple "500 Server Error" message, or a "404 Not Found" if there was a routing error, or a record could not be found.
You can customize how these errors are caught and how they're displayed to the
user. There are several levels of exception handling available in a Rails
application. You can use config.action_dispatch.show_exceptions
configuration
to control how Rails handles exceptions raised while responding to requests. You
can learn more about the levels of exceptions in the
configuration guide.
9.1 The Default Error Templates
By default, in the production environment the application will render an error
page. These pages are contained in static HTML files in the public folder, in
404.html
, 500.html
, etc. You can customize these files to add some extra
information and styles.
The error templates are static HTML files so you can't use ERB, SCSS, or layouts for them.
9.2 rescue_from
You can catch specific errors and do something different with them by using the
rescue_from
method. It can handle exceptions of a certain type (or
multiple types) in an entire controller and its subclasses.
When an exception occurs which is caught by a rescue_from
directive, the
exception object is passed to the handler.
Below is an example of how you can use rescue_from
to intercept all
ActiveRecord::RecordNotFound
errors and do something with them:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found
render plain: "Record Not Found", status: 404
end
end
The handler can be a method or a Proc
object passed to the :with
option. You
can also use a block directly instead of an explicit Proc
object.
The above example doesn't improve on the default exception handling at all, but it serves to show how once you catch specific exceptions, you're free to do whatever you want with them. For example, you could create custom exception classes that will be thrown when a user doesn't have access to a certain section of your application:
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
# Check that the user has the right authorization to access clients.
before_action :check_authorization
def edit
@client = Client.find(params[:id])
end
private
# If the user is not authorized, throw the custom exception.
def check_authorization
raise User::NotAuthorized unless current_user.admin?
end
end
Using rescue_from
with Exception
or StandardError
would cause
serious side-effects as it prevents Rails from handling exceptions properly. As
such, it is not recommended to do so unless there is a strong reason.
Certain exceptions are only rescuable from the ApplicationController
class, as they are raised before the controller gets initialized, and the action
gets executed.