Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save namnv609/ff9cc412938f9aebbee5899ba4827b26 to your computer and use it in GitHub Desktop.
Save namnv609/ff9cc412938f9aebbee5899ba4827b26 to your computer and use it in GitHub Desktop.
Notes for action controller overview from Rails official site. rails 4.2

#Action Controller Overview

##0. Mass Assignment

  1. Mass assignment allows us to set a bunch of attributes at once:

    attrs = {	:first => "John", :last => "Doe", 
    			:email => "[email protected]"}
    user = User.new(attrs)
    
    # Without the convenience of mass assignment, 
    # we'd have to write an assignment statement for each attribute 
    # to achieve the same result.
    user.first = attrs[:first]
    user.last  = attrs[:last]
    user.email = attrs[:email]

    http://code.tutsplus.com/tutorials/mass-assignment-rails-and-you--net-31695

##1. GET and POST

  1. GET request parse data by query string which is appended to the URL. The length of query string is limited to 2048 characters. And all chars must be ASCII code chars.

    RFC3986 - URI format

    foo://example.com:8042/over/there?name=ferret#nose
    \_/   \______________/\_________/ \_________/ \__/
    |           |            |            |        |
    scheme   authority      path        query   fragment
    |   _____________________|__
    / \ /                        \
    urn:example:animal:ferret:nose
  2. The params are seperated by & symbol, blank space is seperated by +.

  3. Post send data by message body. It usually set Content-type to either application/x-www-form-urlencoded, or multipart/form-data.

  4. When you receive a POST request, you should always expect a payload(message body in HTTP terms) which have sending data. The data format will be the same as GET request's query string.

  5. POST request data size can be set by HTTP server's configuration.

    How are parameters sent in an http post request.

##2. Params

  1. All sending data will be parsed to params hash in rails.

    # CASE 1
    #
    # GET clients: /clients?status=activated
    params[:status] == "activated"
    
    
    # CASE 2
    #
    # POST /clients, form does not custom its sending data to a AR model.
    <%= form_tag 'clients', method: "POST" do %>
    	<%= text_field_tag 'name' %>
    	<%= submit_tag "Send Client" %>
    <% end %>
    
    params
    # =>
    # { "name"=>"asdas",
    #   "commit"=>"Send Client",
    #   "controller"=>"welcome",
    #   "action"=>"client_create" }
    
    
    # CASE 3
    #
    # POST /clients, form data is customed to a user model
    <%= form_for :user, url: "/clients" do |f| %>
    	<%= f.text_field 'name' %>
    	<%= f.submit "Send" %>
    <% end %>
    
    # HTML code
    <form action="/clients" accept-charset="UTF-8" method="post">
      <input type="text" name="user[name]" id="user_name"><br>
      <input type="text" name="user[email]" id="user_email"><br>
      <input type="password" name="user[password]" id="user_password"><br>
      <input type="submit" name="commit" value="register">
    </form>
    
    params
    # =>
    # { "user" => {  "name"=>"ada", 
    #	 			 "email"=>"[email protected]",
    #	 			 "password"=>"123456789"
    #			  },
    # 	 "commit"=>"register",
    # 	 "controller"=>"welcome",
    # 	 "action"=>"client_create"	 }

####2.1 ERB trim space

What is the difference between <%, <%=, <%# and -%> in erb in rails.

####2.2 Hash and Array in params

  1. To send an array of values, append an empty pair of square brackets "[]" to the key name:

    # GET /clients?ids[]=1&ids[]=2&ids[]=3
    
    # The actual URL in this example will be encoded as:
    # "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" 
    # as the "[" and "]" characters are not allowed in URLs. 
    # But browser will encode, decode for you.
    
    params[:ids] = [1, 2, 3]
    
    
    # To send a hash, include a key name inside the brackets.
     
    <form accept-charset="UTF-8" action="/clients" method="post">
    	<input type="text" name="client[name]" value="Acme" />
    	<input type="text" name="client[phone]" value="123" />
    	<input type="text" name="client[address][postcode]" value="123" />
    	<input type="text" name="client[address][city]" value="City" />
    </form>
    
    params[:client]
    # { "name" => "Acme", 
    #   "phone" => "12345", 
    #   "address" => { "postcode" => "12345", "city" => "Carrot City" } 
    # }

####2.3 JSON parameters

  1. If the Content-Type header of your request is set to application/json, Rails will automatically load your parameters into the params hash.

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

    To:

    params[:company] == / 
    { "name": "acme", "address": "123 Carrot Street"  }
  2. If you've turned on config.wrap_parameters in your initializer or called wrap_parameters in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON POST can be written as:

    // JSON input
    { "name": "acme", "address": "123 Carrot Street" }

    Assuming that you're sending the data to CompaniesController, params is to:

    # params will have
    { 	name: "acme", 
    	address: "123 Carrot Street", 
    	company: { name: "acme", address: "123 Carrot Street" } }
  3. Parsing XML parameters will need actionpack-xml_parser gem.

####2.4 Routing Parameters

  1. params hash always have controller and action keys. But you should access them by controller_name and action_name instead.

  2. You can also add key value pairs to params hash and dynamically setting route param in your routes.rb.

    # routes.rb
    get 'clients/:status' => "welcome#get_index", foo: 'bar'
    
    # GET request "clients/bb"
    
    params
    # => {	"foo"=>"bar", "controller"=>"welcome", 
    #		"action"=>"get_index", "status"=>"bb"}
    
    controller_name
    # => "welcome"
    
    action_name
    # => "get_index"

####2.5 Default URL Options

  1. You can also add query strings to your URL request. Define a default_url_options method in your controller and must return hash.

  2. For performance reasons, the returned hash is cached, there is at most one invocation per request.

  3. That query string will append to any URL generation methods, such as generate action path for form_for method, or url_for method. It can be overridden by options passed to url_for.

  4. That hash will set its key-value pairs to params when GET/POST request comes inot server.

    # application_controller.rb
    def	default_url_options
    	{ hello: "default_url_options" }
    end
    
    # routes.rb
    post '/' => "welcome#create"
    
    
    # request will be actually as: POST "/?hello=default_url_options"
    params
    # => { "user"=>{"name"=>"", "email"=>"[email protected]", "password"=>"123"},
    #	   "commit"=>"register",
    #      "hello"=>"default_url_options",
    #      "controller"=>"welcome",
    #      "action"=>"create"
    #    }
  5. We can override the url default options in our customed controller. Which will take place the method in application controller.

    # welcome_contrller.rb
    def	default_url_options
    	if request.env["HTTP_ACCEPT_LANGUAGE"].include? "en"
    		{ my_hello: "my_default_url_options" }
    	else
    		{ localization: "none" }
    	end
    end
    
    # form in html code, action has appended that query string.
    <form action="/?my_hello=my_default_url_options" 
    	accept-charset="UTF-8" method="post">
    # ...
    </form>
    
    params
    # {	 "user"=>{"name"=>"", "email"=>"[email protected]",
    #	 "password"=>"123"}, "commit"=>"register",
    #	 "my_hello"=>"my_default_url_options"
    # } 

2.6 Strong Parameters

  1. Using strong parameters to prevent model's unallowed attrtibutes accidentally accessed by users.

    #####Must Read: Strong parameters by example. #####http://patshaughnessy.net/2014/6/16/a-rule-of-thumb-for-strong-parameters

  2. If we missing required param, server will throw exception and return 400 bad request by default.

  3. require a param means it requires a key(model) and filter out other keys. permit accept the permitted scalar values only, so hash or array can not be directly accepted by permit.

  4. To get attribute with array value from permit, we can declare as below:

    params = ActionController::Parameteres.new(usernames: ["John", "Las"])
    params.permit(usernames: [])
  5. To get attribute with hash value from permit, we can declare as below:

    params = ActionController::Parameteres.new(usernames: 
    	{"a" => 1, "b" => 2})
    params.permit(usernames: [:a, :b])
  6. Though require or require.permit may produce same hash result, but it may raise ActiveModel::ForbiddenAttributesError if without permit.

    params = ActionController::Parameters.new(user: { username: "john" })
    params.require(:user)
    # => { "username" => "john" }
    params.require(:user).permit(:username)
    # => { "username" => "john" }
    
    params.require(:user).permitted?
    # => false
    # When you inject to mass assignment,
    # This raise ActiveModel::ForbiddenAttributesError.  
    
    User.new(params.require(:user))
    # ActiveModel::ForbiddenAttributesError
    
    params.require(:user).permit(:username).permitted?
    # => true
  7. Strong parameters must be permitted. It can be required to specific attributes in params.

  8. Using require with permit will only return the values, or nested hash, but not the parent one.

    params = ActionController::Parameters.new(user: { username: "john" })
    params.permit(user: [ :username ])
    # => { "user" => {"username"=>"john"} }
    
    params.require(:user).permit(:username)
    # => { "username" => "john" }
  9. require returns the actual value of the parameter, unlike permit which returns the actual hash.

    params = ActionController::Parameters.new(username: "john", 
    	password: {a: 1})
    params.require(:usernames)
    # "John"
    
    params.require(:password)
    # {a: 1}
    
    params.permit(:usernames)
    # { usernames: "john"}
    
    params.permit(password: [:a])
    # { password: {a: 1}}
  10. Sometimes we can not unsure the attributes in nested hash, ex: JSON data. Then we need to use tap workaround this. So we can mixup other values which out side the scope of strong parameters:

    params = ActionController::Parameters.new(user: 
    				{ username: "john", data: { foo: "bar" } })
    
    # let's assume we can't do below,
    # because the data hash can contain any kind of data
    params.require(:user).permit(:username, data: [ :foo ])
    
    # we need to use the power of ruby to do this "by hand"
    params.require(:user).permit(:username).tap do |whitelisted|
    	whitelisted[:data] = params[:user][:data]
    end
    # Unpermitted parameters: data
    # => { "username" => "john", "data" => {"foo"=>"bar"} }
    
    # Or mixup out side the scope of strong parameters.
    def product_params
    	params.require(:product).permit(:name, 
    			data: params[:product][:data].try(:keys))
    end
  11. Only one key can be required, but multiple permitted scalar values can be permitted.

    # GET /?name[]=1&name[]=2&name[]=3&kk[a]=1&kk[b]=2
    
    params
    # => {	"utf8"=>"✓",
    # 		"authenticity_token"=>"Hbw===",
    #		"name"=>["1", "2", "3"],
    #		"kk"=>{"a"=>1, "b"=>2, "c"=> [3,4,5]}},
    #		"commit"=>"Send Client",
    #		"controller"=>"welcome",
    #		"action"=>"client_create" }
    
    params.require(:name)
    # => ["1", "2", "3"]
    
    # permit not accept key's value is hash or array.
    params.permit(:name)
    # ActionController::UnpermittedParameters: 
    # found unpermitted parameters: 
    #	utf8, authenticity_token, name, commit, kk
    
    params.require(:action)
    # => "client_create"
    
    params.require(:action, :controller)
    # => ArgumentError: wrong number of arguments (2 for 1)
    
    # permit an array
    params.permit(name: [])
    # [1, 2, 3]
  12. By default, config.action_controller.action_on_unpermitted_parameters is set to :log. It will omit the unpermitted attributes and give warning only. You can respond it by set the property to :rasie. This will lead the server respond back 500 sever internal error request.

    # application.rb, deveopment.rb, or any environment.rb
    config.action_controller.action_on_unpermitted_parameters == :log
    # default is :log
    
    #welcome_controller.rb
    class WelcomeController < ApplicationController
    	def	index
    		user_params
    	end
    	
    	private
    	
    	def	user_params
    		params.permit(:name, :password)
    	end
    end
    
    # GET /?email=a.mail.com&name=&password=1234
    
    user_params
    # Unpermitted parameter: email
    # => {"name"=>"", "password"=>"1234"}
    
    # Now set :log to :raise
    config.action_controller.action_on_unpermitted_parameters = :raise
    # When we call user_params in our controller, it will raise exception.
    
    user_params
    # ActionController::UnpermittedParameters: 
    # found unpermitted parameter: email.
    # And return 500 server internal error response.
  13. If your required parameters is missing, then you can rescue the exception and customized the response by applying class method rescue_from in your controller to respond the request in your way.

    # welcome_controller.rb
    class WelcomeController < ApplicationController
    	rescue_from ActionController::ParameterMissing do |param_miss_excp|
    		render :text => "Required parameter missing:" + 
    						"#{parameter_missing_exception.param}", 
    			   :status => :bad_request
    	end
    end
    
    # GET /?email=a.mail.com&name=&password=1234
    
    params.require(:user)
    # ActionController::ParameterMissing: 
    #					param is missing or the value is empty: user
    # Then rescued to 400 bad request.
  14. To Permit an Array with hash values, use hash brackets or none:

    params = ActionController::Parameters.new(
    	title: "post", comment: [{text: "hi"}, {text: "Hi 2"}  ])
    #=> {"title"=>"post", "comment"=>[{"text"=>"hi"}, {"text"=>"Hi 2"}]}
    
    # permit key comment's array value
    params.permit(:title, { comment: [:text] })
    # or
    params.permit(:title, comment: [:text])
    
    params.permit(:title, {comment: [:text]}) == 
    	params.permit(:title, comment: [:text])
    # true

##3. Session

  1. 4 ways to store sessions:

    ActionDispatch::Session::CookieStore 
    # Stores everything on the client.
    
    ActionDispatch::Session::CacheStore 
    # Stores the data in the Rails cache.
    
    ActionDispatch::Session::ActiveRecordStore 
    # Stores the data in a database using Active Record.
    # (require activerecord-session_store gem).
    
    ActionDispatch::Session::MemCacheStore
    # Stores the data in a memcached cluster 
    # (this is a legacy implementation; consider using CacheStore instead).
  2. All session stores use a cookie to store a unique ID for each session (you must use a cookie, Rails will not allow you to pass the session ID in the URL as this is less secure).

  3. For most stores, this ID is used to look up the session data on the server, e.g. in a database table. There is one exception, and that is the default and recommended session store - the CookieStore - which stores all session data in the cookie itself.

    The cookie data is cryptographically signed to make it tamper-proof. And it is also encrypted so anyone with access to it can't read its contents. Rails will not accept it if it has been edited.

  4. The CookieStore can store around 4kB of data. Storing large amounts of data in the session is discouraged no matter which session store your application uses.

  5. You should especially avoid storing complex objects (anything other than basic Ruby objects, the most common example being model instances) in the session, as the server might not be able to reassemble them between requests, which will result in an error.

  6. Consider ActionDispatch::Session::CacheStore if your user sessions don't store critical data or don't need to be around for long periods. The downside is that the sessions will be ephemeral and could disappear at any time.

  7. Change session store mechanism:

    # 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 by "rails g active_record:session_migration"
    
    Rails.application.config.session_store :active_record_store
    
    # 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"
  8. Session is server side hash which locate its session id as key to identify user and user id.

  9. Cookie is a file store in client side, but each GET request header has Set-Cookie field which can modify cookie from server to client side response. Each client request server by setting session id in Cookie field to let server identify its user id.

  10. All the session data in cookie by Session::CookieStore has been encrypted.

    Demo::Application.config.session_store :cookie_store, key: '_demo_session'
    
    session[:github_username] = 'neerajdotname'
    
    # check cookie with _demo_session key
    # "BAh7CEkiD3Nlc3...==--b5bcce534ceab56616d4a215246e9eb1fc9984a4"
    
    # unescaped chars
    unescaped_content = URI.unescape(content)
    # => "BAh7CEkiD3Nlc3...==--b5bcce534ceab56616d4a215246e9eb1fc9984a4"
    
    # seperate from data and digest
    data, digest = unescaped_content.split('--')
    # => ["BAh7CEkiD3Nlc...==", "b5bcce534ceab56616d4a215246e9eb1fc9984a4"]
    
    Marshal.load(::Base64.decode64(data))
    # => {	"session_id"=>"80dab78baffa77655fef0e13a3ba208a",

"github_username"=>"neerajdotname",

"_csrf_token"=>"MJL+6uugDZ6GcStnJoq6vnArVXDbFn2uMvDSK0jlrYM="}

```

####3.1 Flash

  1. The flash is a special part of the session which is cleared with each request. This means that values stored there will only be available in the next request, which is useful for passing error messages etc.

    class LoginsController < ApplicationController
    	def destroy
    		session[:current_user_id] = nil
    		flash[:notice] = "You have successfully logged out."
    		redirect_to root_url
    		
    		# or customized in redirection method
    		redirect_to root_url, notice: "You have logged out."
    		redirect_to root_url, alert: "You're stuck here!"
    		redirect_to root_url, flash: { referral_code: 1234 }
    	end
    end
  2. You can then display the flash message in view template:

    <html>
    	<!-- <head/> -->
    	<body>
    		<% flash.each do |name, msg| -%>
      			<%= content_tag :div, msg, class: name %>
    		<% end -%>
    
    		<p class="well"><%= flash[:alert] %></p>
    
    		<!-- more content -->
    	</body>
    </html>
  3. If you want a flash value to be carried over to another request, use the keep method:

    class MainController < ApplicationController
    	# Let's say this action corresponds to root_url, but you want
    	# all requests here to be redirected to UsersController#index.
    	# If an action sets the flash and redirects here, the values
    	# would normally be lost when another redirect happens, but you
    	# can use 'keep' to make it persist for another request.
    	def index
    		# Will persist all flash values.
    		flash.keep
    
    		# You can also use a key to keep only some kind of value.
    		# flash.keep(:notice)
    		redirect_to users_url
    	end
    end
  4. By default, adding values to the flash will make them available to the next request(redirection), but sometimes you may want to access those values in the same request(render to response).

    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

4. Cookies

  1. Instead of stroing and encrypted data in session then using Session::CokkieStore to store it to cookie, you can just put the data to cookie instead.

    class CommentsController < ApplicationController
    	# Auto-fill the commenter's name if it has been stored in a cookie
    	def new
    		@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
  2. Note that while for session values you set the key to nil, to delete a cookie value you should use cookies.delete(:key).

  3. To know more about how cookie store sensetive data and data transform with JSON format, check cookie jar in that subpart:

    http://edgeguides.rubyonrails.org/action_controller_overview.html#cookies

5. Rendering XML and JSON data

  1. Instead redirect_to and render helpers, call respond_to:

    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

6. Filters

  1. We can add filter methods before or after actions.

    class ApplicationController < ActionController::Base
    	before_action :require_login
    	after_action :forward_notifications, only: :create
    
    	def create; end
    
    	private
    
    	def require_login
    		unless logged_in?
      			flash[:error] = "You log in to access this section"
      			redirect_to new_login_url # halts request cycle
    		end
    	end
    	
    	def forward_notifications	
    		# after action cannot stop that action,
    		# but can do some background jobs.
    	end
    end
  2. "around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work.

    class ChangesController < ApplicationController
    	around_action :wrap_in_transaction, only: :show
    
    	private
    
    	def wrap_in_transaction
    		ActiveRecord::Base.transaction do
      			begin
        			yield	# yield to that show action code.
      			ensure
        			raise ActiveRecord::Rollback
      			end
    		end
    	end
    end
  3. We can also use filters by *_action block, around action also needs yield:

    class ApplicationController < ActionController::Base
    	# private instance method :logged_in?
    	before_action do |controller|
    		unless controller.send(:logged_in?)
      			flash[:error] = "You logged in to access this section"
      			redirect_to new_login_url
    		end
    	end
    end
  4. Or we can use a filter class. The filter class must implement a method with the same name as the filter, so for the before_action filter, the class must implement a before class method, and so on. The around method must yield to execute the action.

    class ApplicationController < ActionController::Base
    	before_action FilterClass
    end
    
    
    class Filter 
    	def	self.before(controller)
    		unless controller.send(:logged_in?)
      			controller.flash[:error] = "You..."
      			controller.redirect_to controller.new_login_url
    		end
    	end
    end
  5. around_action block:

    around_action do |controller|
    	if params[:action] == "create"
      		check_creation controller
    	else
      		controller.send(params[:action])
    	end
    end
    
    def	create; end
    
    def check_creation(controller)
    	puts (params["action"]).append(" around action!")
    	# create action
    	controller.send(params["action"])
    	puts "Done the around"
    end
    
    # Log:
    # create around action!
    # begin transaction
    # 		...
    # commit transaction
    # Redirected to http://localhost:3000/?my_hello=my_default_url_options
    # Done the around
    # Completed 302 Found in 38379ms (ActiveRecord: 9.7ms)

7. Request Forgery Protection

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

  2. Rails add hidden field to form which prevent csrf attack.

8. Request and Response Object

  1. We can call request or response object to know the header or any other details in that request/response.

    class SomeController < ApplicationController
    	def	index
    		p request.env["QUERY_STRING"]
    	end
    end
  2. 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.

    request.query_parameters
    # => {"my_hello"=>"my_default_url_options"}
    
    request.path_parameters
    # => {:controller=>"welcome", :action=>"create"}
    
    request.request_parameters
    # parameters from message body
    # => {	"utf8"=>"✓",
    #		"authenticity_token"=>"CQ==",
    #		"user"=>{"name"=>"", "email"=>"[email protected]",
    # 		"password"=>"123456789"},
    # 		"commit"=>"register"}
    
    params
    # {	"utf8"=>"✓",
    #	"authenticity_token"=>"CQ==",
    # 	"user"=>{"name"=>"", "email"=>"[email protected]", 
    # 	"password"=>"123456789"},
    # 	"commit"=>"register",
    # 	"my_hello"=>"my_default_url_options",
    #	"controller"=>"welcome",
    #	"action"=>"create"}
  3. You can customized your repsonse with customed headers. If you want to add or change a header, just assign it to response.headers this way, Rails will set some of them automatically:

    response.header["Content-type"] = "application/pdf"
  4. Beware if you have redirect_to or render helpers which will override your repsonse settings, put your customized response setting to last line of your action method.

9. HTTP Authentications

  1. Two basic authentications:

    Basic Authentication
    Digest Authentication

  2. 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 by http_basic_authenticate_with.

    class AdminsController < ApplicationController
    	http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
    end
  3. 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). So the actual password digest is encrypted and decrypted in server side only.

  4. It still pop dialog window, but the password can be decrypted later in server side with ha1 digest hash.

    require 'digest/md5'
    class PostsController < ApplicationController
    	REALM = "SuperSecret"
    	USERS = {"dhh" => "secret", #plain text password
           "dap" => Digest::MD5.hexdigest(["dap", REALM, "secret"].join(":"))}  
           #ha1 digest password
    
    before_action :authenticate, except: [:index]
    
    def index
    	render plain: "Everyone can see me!"
    end
    
    def edit
    	render plain: "I'm only accessible if you know the password"
    end
    
    private
    	def authenticate
      		authenticate_or_request_with_http_digest(REALM) do |username|
      			USERS[username]
      		end
    	end
    end
  5. The authenticate_or_request_with_http_digest block must return the user's password or the ha1 digest hash. Returning false or nil from the block will cause authentication failure.

10. Streaming and File Downloads

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

  2. For send_data, 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.

    #####http://apidock.com/rails/ActionController/Streaming/send_data

    # 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
  3. send_file 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.

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

  5. In REST terminology, the PDF file from the example above can be considered just another representation of the client resource. 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
  6. Inorder to make above code work, you have to register MIME type to Rails:

    Configuration files are not reloaded on each request, so you have to restart the server in order for their changes to take effect.

    # config/initializers/mime_types.rb
    Mime::Type.register "application/pdf", :pdf
  7. Now the user can request to get a PDF version of a client just by adding ".pdf" to the URL:

    GET /clients/1.pdf

10.1 Live Streaming

  1. The ActionController::Live module allows you to create a persistent connection with a browser.

    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
  2. Forgetting to close the stream will leave the socket open forever. We also have to set the content type to text/event-stream before we write to the response stream. This is because headers cannot be written after the response has been committed (when response.committed? returns a truthy value), which occurs when you write or commit the response stream.

  3. Another Example, show the lyrics on time:

    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
  4. Stream Considerations:

    1. 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.
    1. 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.
    2. WEBrick servers buffer all responses, and so including ActionController::Live will not work. You must use a web server which does not automatically buffer responses.

11. Log Filtering

  1. Sometime you don't want to log too much in production env.

    # filter out sensetive request parameters
    config.filter_parameters << :password
    
    # Sometimes it's desirable to filter out from log files 
    # some sensitive locations your application is redirecting to.
    config.filter_redirect << 's3.amazonaws.com'
    
    config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]
    # Matching URLs will be marked as '[FILTERED]'

12. Rescue

  1. When excpetion throwed, Rails will 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.

  2. The default 500 or 404 message are a staitc html file in public folder. They are static, you can customized but not able to use any template engine to it(ERB, Haml, Slim).

  3. To address these error message, call rescue_from with block or :with method:

    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
  4. You can also create a customed Excpetion class, which raise an exception and rescued later:

    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
  5. Certain exceptions are only rescuable from the ApplicationController class, as they are raised before the controller gets initialized and the action gets executed.

13. Force HTTPS protocol

  1. You can use the force_ssl method in your particular controller to enforce that, provide only or except to limit the HTTPS to particular action:

    class DinnerController
    	force_ssl only: :create
    	# or
    	force_ssl except: :index
    end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment