Dans ce guide, vous allez apprendre comment les contrôleurs fonctionnent et comment s'en servir dans le cycle de votre application.
Après avoir lu ce guide, vous saurez :
- Comment suivre le parcours d'une requête à travers un contrôleur ;
- Pourquoi et comment enregistrer des données en session ou dans les cookies ;
- Comment travailler avec les filtres pour exécuter du code pendant le traitement de la requête ;
- Comment utiliser l'authentification HTTP avec Action Controller ;
- Comment envoyer en continue des données au navigateur ;
- Comment filtrer les paramètres sensibles pour ne pas qu'ils apparaissent dans les logs ;
- Comment traiter les exceptions qui seraient levées durant le traitement de la requête.
Action Controller est le C dans MVC. Après que le routage ait déterminé quel contrôleur utiliser pour une requête, votre contrôleur doit faire le nécessaire pour sortir un résultat en lien avec la requête. Par chance, Action Controller fait le travail de base et utilise des conventions intelligentes pour rendre le reste le plus simple et rapide que possible.
Pour les applications de type RESTful, le contrôleur reçoit la requête (c'est invisible pour les développeurs), récupère ou sauvegarde les données du modèle et utilise la vue pour générer le HTML. Si votre contrôleur a besoin de faire les choses un peu différemment, ce n'est pas un problème, c'est juste la façon la plus conventionnelle pour le contrôleur de travailler.
Un contrôleur fait le lien entre les modèles et les vues. Il rend disponibles les données du modèle pour la vue, ce qui nous permet de les afficher, puis il enregistre ou met à jour le modèle avec les données fournies par l'utilisateur.
Pour plus de détails sur le fonctionnement du routage : Rails Routing from the Outside In.
Un contrôleur est une classe Ruby qui hérite de ApplicationController
et a des méthodes comme tout autre classe. Quand votre application reçoit une demande, le routage déterminera quel contrôleur et action exécuter, puis Rails crée une instance de ce contrôleur et appelle la méthode qui a le même nom que l'action.
class ClientsController < ApplicationController
def new
end
end
Pour exemple, si un utilisateur va vers /clients/new
dans notre application pour ajouter un nouveau client, Rails créera une instance de ClientsController
et appellera la méthode new
. Notez bien que la méthode vide utilisée dans l'exemple fonctionne car Rails rendra par défaut la vue new.html.erb
sauf si l'action dit de faire différemment. La méthode new
pourrait rendre visible pour la vue la variable d'instance @client
en créant un nouveau Client
:
def new
@client = Client.new
end
Le Layouts & Rendering Guide explique ce principe plus en détail.
ApplicationController
hérite de ActionController::Base
qui définit un certain nombre de méthodes bien utiles. Ce guide reviendra sur certaines d'entre elles, mais si vous êtes curieux de voir ce qu'elles font, vous pouvez toutes les retrouver dans la documentation de l'API ou dans les sources.
Seules les méthodes publiques sont appelées en tant qu'actions. C'est une bonne pratique de restreindre la visibilité des méthodes qui ne sont pas des actions, comme les méthodes auxiliaires ou les filtres.
Vous voudrez surement accéder aux données envoyées par l'utilisateur ou à d'autres paramètres présents dans le contrôleur. Il y a deux types de paramètres possible dans une application web. Les premiers sont les paramètres qui font partie de l'URL, appelé paramètres de query string. les query string parameters sont tout ce qui se trouve après le "?" dans l'URL. Le second type de paramètre fait référence aux données envoyées par la méthode "POST". Cette information vient le plus souvent d'un envoi de formulaire HTML qui a été rempli par l'utilisateur. C'est appelé "POST" car cela peut uniquement être envoyé en tant que partie d'une requête HTTP de type "POST". Rails ne fait pas de distinction entre les paramètres de query string et les POST parameters, les deux sont disponibles dans le hash params
de votre contrôleur :
class ClientsController < ActionController::Base
# Cette action utilise des paramètres de query string car
# elle est exécutée par une requête HTTP de type GET,
# mais cela ne fait aucune différence en ce qui concerne leur accès.
# Pour afficher la liste des clients activés, l'URL ressemblerait à :
# /clients?status=activated
def index
if params[:status] == "activated"
@clients = Client.activated
else
@clients = Client.inactivated
end
end
# Cette action utilise les paramètres de type POST. Ils viennent le
# plus souvent d'un formulaire HTML que l'utilisateur a envoyé. l'URL pour
# cette requête RESTful est "/clients" et les données seront
# envoyées dans le corps de la requête.
def create
@client = Client.new(params[:client])
if @client.save
redirect_to @client
else
# Cette ligne surcharge le comportement de rendu par défaut
# qui aurait normalement été le rendu de la vue "create".
render "new"
end
end
end
Les params
de type hash ne sont pas limités aux clefs et valeurs à une dimension. Il peuvent contenir des tableaux et des hashs imbriqués. Pour envoyer un tableau de valeur, ajoutez une paire de crochets vide "[]" après le nom de la clef :
GET /clients?ids[]=1&ids[]=2&ids[]=3
À Noter : en réalité l'URL de cet exemple serait encodé comme ceci "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" car "[" et "]" ne sont pas autorisés dans les URL. La plupart du temps il est inutile de s'inquiéter à propos de ça car le navigateur s'en charge pour vous, et Rails le décodera en retour quand il le recevra, mais si jamais vous avez besoin d'envoyer ces requêtes manuellement à un serveur vous devez avoir ça en tête.
La valeur du params[:ids]
sera maintenant ["1", "2", "3"]
. À noter que les valeurs de paramètres sont toujours du type String
; Rails ne fait rien pour deviner ou transtyper le type.
Pour envoyer un hash vous devez inclure le nom de la clef dans les crochets :
<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>
Quand ce formulaire est envoyé, la valeur de params[:client]
serait {"name" => "Acme", "phone" => "12345", "address" => {"postcode" => "12345", "city" => "Carrot City"}}
. Notez le hash imbriqué dans params[:client][:address]
.
Remarquez que le params
de type hash est en réalité une instance de HashWithIndifferentAccess
d'Active Support, qui se comporte comme un hash et vous laisse utiliser des symboles ou des chaines de caractères indifféremment comme clefs.
Si vous écrivez une application web, vous pourriez vous retrouver plus à l'aise d'accepter des paramètres aux format JSON ou XML. Rails convertira automatiquement vos paramètres en params
de type hash, que vous pourrez accéder comme vous le feriez avec des données d'un formulaire.
Donc pour exemple, si vous envoyez ces paramètres JSON :
{ "company": { "name": "acme", "address": "123 Carrot Street" } }
Vous aurez params[:company]
qui donnera { :name => "acme", "address" => "123 Carrot Street" }
.
Aussi, si vous activez config.wrap_parameters
dans votre initialisation ou appelez wrap_parameters
dans votre contrôleurs, vous pouvez omettre en toute sécurité l'élément racine dans le paramètre au format JSON/XML. Les paramètres seront clonés et entouré de la clef selon le nom du contrôleur par défaut. Donc le paramètre suivant peut être écrit comme ceci :
{ "name": "acme", "address": "123 Carrot Street" }
Et supposons que vous envoyez les données à CompaniesController
, il serait alors enveloppé dans la clef :company
comme ceci :
{ :name => "acme", :address => "123 Carrot Street", :company => { :name => "acme", :address => "123 Carrot Street" }}
Vous pouvez personnaliser le nom de la clef ou des paramètres spécifiques que vous voulez envelopper en consultant la documentation de l'API
Le hash params
contiendra toujours les clefs :controller
et :action
, mais vous devriez utiliser les méthodes controller_name
et action_name
à la place pour accéder à ces valeurs. Tous les autres paramètres définis par le routage, comme :id
sera aussi disponible. Par exemple, considérez un listing de clients où la liste peut montrer à la fois des clients actifs et inactifs. Nous pouvons ajouter une route qui capture le paramètre :status
dans une "jolie" URL :
match '/clients/:status' => 'clients#index', foo: "bar"
Dans ce cas, quand un utilisateur ouvre l'URL /clients/active
, params[:status]
aura pour valeur "active". Quand cette route est utilisée, params[:foo]
aura pour valeur "bar" de la même façon que si ça avait été passé par les paramètres de query string. De la même façon params[:action]
contiendra la valeur "index".
Vous pouvez initialiser un paramètre global par défaut pour le générateur d'URL en définissant une méthode appelé default_url_options
dans votre contrôleur. Cette méthode doit renvoyer un hash avec les valeurs par défauts voulu, ces clefs doivent être des symboles :
class ApplicationController < ActionController::Base
def default_url_options
{ locale: I18n.locale }
end
end
Ces options seront utilisées comme point de départ lors de la génération des URLs, il est donc possible qu'ils soient remplacés par les options passées dans la méthode url_for
.
Si vous définissez default_url_options
dans ApplicationController
, comme dans l'exemple ci-dessus, il sera utilisé pour toutes les générations d'URL. La méthode peut également être définie dans un contrôleur spécifique, dans ce cas, il affecte uniquement les URLs générées du concernant ce contrôleur.
Votre application a une session pour chaque utilisateur dans laquelle vous pouvez stocker de petites quantités de données qui seront persisté entre les requêtes. La session est uniquement disponible dans le contrôleur et la vue et peut utiliser l'un de ces mécanismes de stockage différents :
ActionDispatch::Session::CookieStore
- Stocke tout dans le navigateur.ActionDispatch::Session::CacheStore
- Stock les données dans le cache de Rails.ActionDispatch::Session::ActiveRecordStore
- Stocke les données dans une base de donnée en utilisant Active Record. (nécessite le gemactiverecord-session_store
).ActionDispatch::Session::MemCacheStore
- Stocke les données dans un cluster de type memcached (C'est une ancienne implémentation; utilisez à la place CacheStore).
Toutes ces façons de stocker une session utilise un cookie pour stocker un identifiant unique pour chaque session (vous devez utiliser un cookie, Rails ne vous permettra pas de passer l'ID de session dans l'URL car cela est moins sûr).
Pour la plupart des enregistrements, cet ID est utilisé pour rechercher les données de session sur le serveur, par exemple, dans une table de base de données. Il y a une exception, et c'est la valeur par défaut et recommandée de stockage de session - par les Cookies - qui stocke les données de session dans le cookie lui-même (l'ID est toujours à votre disposition si vous en avez besoin). Ceci a l'avantage d'être très léger et ne nécessite aucune installation sur une nouvelle application pour utiliser la session. Les données cookie ont une signature cryptographique pour le rendre inviolable, mais il n'est pas cryptée, donc toute personne ayant accès à elle peut lire son contenu mais pas le modifier (Rails ne l'acceptera pas s'il a été modifié).
L'enregistrement par cookie peut stocker environ 4 Ko de données - beaucoup moins que les autres - mais cela est généralement suffisant. Stocker de grandes quantités de données dans la session est déconseillée quel que soit le stockage de session utilisé par votre application. Vous devez surtout éviter de stocker des objets complexes (autre chose que des objets de base de Ruby, l'exemple le plus courant étant instances du modèle) dans la session, le serveur peut ne pas être en mesure de les réhabiliter entre les demandes, ce qui se traduira par une erreur.
Si vos sessions utilisateurs ne stockent pas de données critiques ou n'ont pas besoin d'être là pour de longues périodes (par exemple, si vous venez d'utiliser un message de type flash), vous pouvez envisager d'utiliser ActionDispatch::Session::CacheStore. Cela stockera les sessions en utilisant l'implémentation de cache que vous avez configuré pour votre application. L'avantage de ceci est que vous pouvez utiliser votre infrastructure existante pour stocker les sessions sans nécessiter de configuration supplémentaire ou de l'administration. L'inconvénient, bien sûr, est que les sessions seront éphémères et pourraient disparaître à tout moment.
En savoir plus sur le stockage des sessions dans le [Guide de sécurité] (security.html)
Si vous avez besoin d'un mécanisme autre pour stocker les sessions, vous pouvez le modifier dans le fichier config/initializers/session_store.rb
:
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with "script/rails g active_record:session_migration")
# YourApp::Application.config.session_store :active_record_store
Rails établit une clé de session (le nom du cookie) lors de la signature des données de session. Ceux-ci peuvent également être modifiés dans config/initializers/session_store.rb
:
# Be sure to restart your server when you modify this file.
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session'
Vous pouvez également passer la clef :domain
et spécifier le nom de domaine pour le cookie :
# Be sure to restart your server when you modify this file.
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
Rails met en place (pour le stockage par cookies) une clé secrète utilisée pour la signature des données de session. Ceci peut être modifié dans config/initializers/secret_token.rb
.
# Be sure to restart your server when you modify this file.
# Your secret key for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'
REMARQUE : La modification du jeton secret lorsque vous utilisez le CookieStore
annulera toutes les sessions existantes.
Dans votre contrôleur, vous pouvez accéder à la session via la méthode session
.
REMARQUE : Les sessions sont chargées si besoin. Si vous n'avez pas accédé à des sessions dans le code de votre action, elles ne seront pas chargés. Par conséquent, vous n'aurez jamais besoin de désactiver les sessions, le fait de ne pas y accéder sera suffisant.
Les valeurs de session sont stockées en utilisant les paires clé/valeur, comme un hash:
class ApplicationController < ActionController::Base
private
# Trouve l'utilisateur avec l'ID stocké dans la session avec la clé
# :current_user_id Il s'agit d'une voie commune pour gérer la connexion de l'utilisateur dans une
# application Rails; se connecter fixe la valeur de session et
# la déconnexion la supprime.
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by_id(session[:current_user_id])
end
end
Pour stocker quelque chose dans la session, il suffit de l'affecter à la clef comme un hash:
class LoginsController < ApplicationController
# "Créé un login" autrement dit "connecter l'utilisateur"
def create
if user = User.authenticate(params[:username], params[:password])
# Sauvegarde de l'ID utilisateur dans la session de sorte qu'il peut être utilisé
# dans les requêtes suivantes
session[:current_user_id] = user.id
redirect_to root_url
end
end
end
Pour supprimer quoique ce soit de la session, assigner la clef à nil
:
class LoginsController < ApplicationController
# "Supprimer" une connexion, autrement dit "déconnecter un utilisateur"
def destroy
# Supprimer l'ID utilisateur de la session
@_current_user = session[:current_user_id] = nil
redirect_to root_url
end
end
Pour réinitialiser toute la session, utilisez reset_session
.
Le flash est une partie spéciale de la session qui est effacée à chaque requête. Cela signifie que les valeurs qui y sont stockées ne seront disponibles que dans la requête suivante, qui est utile pour transmettre des messages d'erreur, etc.
Il est accessible en grande partie de la même manière que la session, comme un hash (c'est une instance de FlashHash).
Profitons de l'acte de se déconnecter à titre d'exemple. Le contrôleur peut envoyer un message qui sera affiché à l'utilisateur sur la demande suivante:
class LoginsController < ApplicationController
def destroy
session[:current_user_id] = nil
flash[:notice] = "You have successfully logged out."
redirect_to root_url
end
end
Notez qu'il est également possible d'assigner un message flash dans le cadre de la redirection. Vous pouvez assigner :notice
, :alert
ou de manière générale :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 }
L'action destroy
redirige vers root_url
de l'application, où le message sera affiché. Notez que c'est à la prochaine action de décider, le cas échéant, ce qu'il faudra faire avec ce que l'action précédente à mise dans le flash.
Par convention nous affichons tous les messages d'erreur ou les informations du flash dans le rendu de l'application :
<html>
<!-- <head/> -->
<body>
<% flash.each do |name, msg| -%>
<%= content_tag :div, msg, class: name %>
<% end -%>
<!-- more content -->
</body>
</html>
De cette façon, si une action renvoie un avis ou un message d'alerte, la mise en page l'affichera automatiquement.
Vous pouvez passer quoi que ce soit que la session peut stocker, vous n'êtes pas limité aux notifications et des alertes.
<% if flash[:just_signed_up] %>
<p class="welcome">Welcome to our site!</p>
<% end %>
Si vous voulez qu'une valeur soit reportées à une autre demande, utilisez la méthode keep
:
class MainController < ApplicationController
# Disons que cette action correspond à root_url, mais vous voulez
# que toutes les demandes soit redirigé vers UsersController#index.
# Si une action met à jour le flash et redirige ici, les valeurs
# serait normalement perdue lors d'une autre redirection, mais vous
# pouvez utiliser 'keep' pour le faire persister pour une autre requete.
def index
# Cela va persister toutes les valeurs de flash.
flash.keep
# Vous pouvez aussi utiliser une clef pour garder uniquement un type de valeur
# flash.keep(:notice)
redirect_to users_url
end
end
By default, adding values to the flash will make them available to the next request, but sometimes you may want to access those values in the same request. For example, if the create
action fails to save a resource and you render the new
template directly, that's not going to result in a new request, but you may still want to display a message using the flash. To do this, you can use flash.now
in the same way you use the normal flash
:
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
if @client.save
# ...
else
flash.now[:error] = "Could not save client"
render action: "new"
end
end
end
Your application can store small amounts of data on the client — called cookies — that will be persisted across requests and even sessions. Rails provides easy access to cookies via the cookies
method, which — much like the session
— works like a hash:
class CommentsController < ApplicationController
def new
# Auto-fill the commenter's name if it has been stored in a cookie
@comment = Comment.new(author: cookies[:commenter_name])
end
def create
@comment = Comment.new(params[:comment])
if @comment.save
flash[:notice] = "Thanks for your comment!"
if params[:remember_name]
# Remember the commenter's name.
cookies[:commenter_name] = @comment.author
else
# Delete cookie for the commenter's name cookie, if any.
cookies.delete(:commenter_name)
end
redirect_to @comment.article
else
render action: "new"
end
end
end
Note that while for session values you set the key to nil
, to delete a cookie value you should use cookies.delete(:key)
.
ActionController makes it extremely easy to render xml
or json
data. If you generate a controller using scaffolding then it would look something like this:
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
Notice that in the above case code is render xml: @users
and not render xml: @users.to_xml
. That is because if the input is not string then rails automatically invokes to_xml
.
Filters are methods that are run before, after or "around" a controller action.
Filters are inherited, so if you set a filter on ApplicationController
, it will be run on every controller in your application.
"Before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way:
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
# The logged_in? method simply returns true if the user is logged
# in and false otherwise. It does this by "booleanizing" the
# current_user method we created previously using a double ! operator.
# Note that this is not common in Ruby and is discouraged unless you
# really mean to convert something into true or false.
def logged_in?
!!current_user
end
end
The method simply stores an error message in the flash and redirects to the login form if the user is not logged in. If a "before" filter renders or redirects, the action will not run. If there are additional filters scheduled to run after that filter, they are also cancelled.
In this example the filter is added to ApplicationController
and thus all controllers in the application inherit it. This will make everything in the application require the user to be logged in in order to use it. For obvious reasons (the user wouldn't be able to log in in the first place!), not all controllers or actions should require this. You can prevent this filter from running before particular actions with skip_before_action
:
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
Now, the LoginsController
's new
and create
actions will work as before without requiring the user to be logged in. The :only
option is used to only skip this filter for these actions, and there is also an :except
option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place.
In addition to "before" filters, you can also run filters after an action has been executed, or both before and after.
"After" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running.
"Around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work.
For example, in a website where changes have an approval workflow an administrator could be able to preview them easily, just apply them within a transaction:
class ChangesController < ActionController::Base
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
Note that an "around" filter also wraps rendering. In particular, if in the example above, the view itself reads from the database (e.g. via a scope), it will do so within the transaction and thus present the data to preview.
You can choose not to yield and build the response yourself, in which case the action will not be run.
While the most common way to use filters is by creating private methods and using *_action to add them, there are two other ways to do the same thing.
The first is to use a block directly with the *_action methods. The block receives the controller as an argument, and the require_login
filter from above could be rewritten to use a block:
class ApplicationController < ActionController::Base
before_action do |controller|
redirect_to new_login_url unless controller.send(:logged_in?)
end
end
Note that the filter in this case uses send
because the logged_in?
method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful.
The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and can not be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class:
class ApplicationController < ActionController::Base
before_action LoginFilter
end
class LoginFilter
def self.filter(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "You must be logged in"
controller.redirect_to controller.new_login_url
end
end
end
Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class has a class method filter
which gets run before or after the action, depending on if it's a before or after filter. Classes used as around filters can also use the same filter
method, which will get run in the same way. The method must yield
to execute the action. Alternatively, it can have both a before
and an after
method that are run before and after the action.
Cross-site request forgery is a type of attack in which a site tricks a user into making requests on another site, possibly adding, modifying or deleting data on that site without the user's knowledge or permission.
The first step to avoid this is to make sure all "destructive" actions (create, update and destroy) can only be accessed with non-GET requests. If you're following RESTful conventions you're already doing this. However, a malicious site can still send a non-GET request to your site quite easily, and that's where the request forgery protection comes in. As the name says, it protects from forged requests.
The way this is done is to add a non-guessable token which is only known to your server to each request. This way, if a request comes in without the proper token, it will be denied access.
If you generate a form like this:
<%= form_for @user do |f| %>
<%= f.text_field :username %>
<%= f.text_field :password %>
<% end %>
You will see how the token gets added as a hidden field:
<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 have to worry about it. If you're writing a form manually or need to add the token for another reason, it's available through the method form_authenticity_token
:
The form_authenticity_token
generates a valid authentication token. That's useful in places where Rails does not add it automatically, like in custom Ajax calls.
The Security Guide has more about this and a lot of other security-related issues that you should be aware of when developing a web application.
In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The request
method contains an instance of AbstractRequest
and the response
method returns a response object representing what is going to be sent back to the client.
The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the API documentation. Among the properties that you can access on this object are:
Property of request |
Purpose |
---|---|
host | The hostname used for this request. |
domain(n=2) | The hostname's first n segments, starting from the right (the TLD). |
format | The content type requested by the client. |
method | The HTTP method used for the request. |
get?, post?, patch?, put?, delete?, head? | Returns true if the HTTP method is GET/POST/PATCH/PUT/DELETE/HEAD. |
headers | Returns a hash containing the headers associated with the request. |
port | The port number (integer) used for the request. |
protocol | Returns a string containing the protocol used plus "://", for example "http://". |
query_string | The query string part of the URL, i.e., everything after "?". |
remote_ip | The IP address of the client. |
url | The entire URL used for the request. |
Rails collects all of the parameters sent along with the request in the params
hash, whether they are sent as part of the query string or the post body. The request object has three accessors that give you access to these parameters depending on where they came from. The query_parameters
hash contains parameters that were sent as part of the query string while the request_parameters
hash contains parameters sent as part of the post body. The path_parameters
hash contains parameters that were recognized by the routing as being part of the path leading to this particular controller and action.
The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values.
Property of response |
Purpose |
---|---|
body | This is the string of data being sent back to the client. This is most often HTML. |
status | The HTTP status code for the response, like 200 for a successful request or 404 for file not found. |
location | The URL the client is being redirected to, if any. |
content_type | The content type of the response. |
charset | The character set being used for the response. Default is "utf-8". |
headers | Headers used for the response. |
If you want to set custom headers for a response then response.headers
is the place to do it. The headers attribute is a hash which maps header names to their values, and Rails will set some of them automatically. If you want to add or change a header, just assign it to response.headers
this way:
response.headers["Content-Type"] = "application/pdf"
Note: in the above case it would make more sense to use the content_type
setter directly.
Rails comes with two built-in HTTP authentication mechanisms:
- Basic Authentication
- Digest Authentication
HTTP basic authentication is an authentication scheme that is supported by the majority of browsers and other HTTP clients. As an example, consider an administration section which will only be available by entering a username and a password into the browser's HTTP basic dialog window. Using the built-in authentication is quite easy and only requires you to use one method, http_basic_authenticate_with
.
class AdminController < ApplicationController
http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end
With this in place, you can create namespaced controllers that inherit from AdminController
. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication.
HTTP digest authentication is superior to the basic authentication as it does not require the client to send an unencrypted password over the network (though HTTP basic authentication is safe over HTTPS). Using digest authentication with Rails is quite easy and only requires using one method, authenticate_or_request_with_http_digest
.
class AdminController < ApplicationController
USERS = { "lifo" => "world" }
before_action :authenticate
private
def authenticate
authenticate_or_request_with_http_digest do |username|
USERS[username]
end
end
end
As seen in the example above, the authenticate_or_request_with_http_digest
block takes only one argument - the username. And the block returns the password. Returning false
or nil
from the authenticate_or_request_with_http_digest
will cause authentication failure.
Sometimes you may want to send a file to the user instead of rendering an HTML page. All controllers in Rails have the send_data
and the send_file
methods, which will both stream data to the client. send_file
is a convenience method that lets you provide the name of a file on the disk and it will stream the contents of that file for you.
To stream data to the client, 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 example above will call a private method which actually generates the PDF document and returns it as a string. This string will then be streamed to the client as a file download and a filename will be suggested to the user. 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 opposite and default value for this option is "attachment".
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
This will read and stream the file 4kB at the time, avoiding 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.
WARNING: Be careful when using data coming from the client (params, cookies, etc.) to locate the file on disk, as this is a security risk that might allow someone to gain access to files they are not meant to.
TIP: 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.
While send_data
works just 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 an easy and quite sleek 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
In order for this example to work, you have to add the PDF MIME type to Rails. This can be done by adding the following line to the file config/initializers/mime_types.rb
:
Mime::Type.register "application/pdf", :pdf
NOTE: Configuration files are not reloaded on each request, so you have to restart the server in order for their changes to take effect.
Now the user can request to get a PDF version of a client just by adding ".pdf" to the URL:
GET /clients/1.pdf
Rails keeps a log file for each environment in the log
folder. These are extremely useful when debugging what's actually going on in your application, but in a live application you may not want every bit of information to be stored in the log file.
You can filter certain request parameters from your log files by appending them to config.filter_parameters
in the application configuration. These parameters will be marked [FILTERED] in the log.
config.filter_parameters << :password
Sometimes it's desirable to filter out from log files some sensible locations 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 marked as '[FILTERED]'.
Most likely your application is going to contain bugs or otherwise throw an exception 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 locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to 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:
By default a production application will render either a 404 or a 500 error message. These messages are contained in static HTML files in the public
folder, in 404.html
and 500.html
respectively. You can customize these files to add some extra information and layout, but remember that they are static; i.e. you can't use RHTML or layouts in them, just plain HTML.
If you want to do something a bit more elaborate when catching errors, you can use rescue_from
, which handles 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. 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.
Here's 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 text: "404 Not Found", status: 404
end
end
Of course, this example is anything but elaborate and doesn't improve on the default exception handling at all, but once you can catch all those 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_to :back
end
end
class ClientsController < ApplicationController
# Check that the user has the right authorization to access clients.
before_action :check_authorization
# Note how the actions don't have to worry about all the auth stuff.
def edit
@client = Client.find(params[:id])
end
private
# If the user is not authorized, just throw the exception.
def check_authorization
raise User::NotAuthorized unless current_user.admin?
end
end
NOTE: Certain exceptions are only rescuable from the ApplicationController
class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's article on the subject for more information.
Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. You can use the force_ssl
method in your controller to enforce that:
class DinnerController
force_ssl
end
Just like the filter, you could also pass :only
and :except
to enforce the secure connection only to specific actions:
class DinnerController
force_ssl only: :cheeseburger
# or
force_ssl except: :cheeseburger
end
Please note that if you find yourself adding force_ssl
to many controllers, you may want to force the whole application to use HTTPS instead. In that case, you can set the config.force_ssl
in your environment file.