respond_to
respond_to(*types, &block)
public
Without web-service support, an action which collects the data for displaying a list of people might look something like this:
def index @people = Person.find(:all) end
Here’s the same action, with web-service support baked in:
def index @people = Person.find(:all) respond_to do |format| format.html format.xml { render :xml => @people.to_xml } end end
What that says is, "if the client wants HTML in response to this action, just respond as we would have before, but if the client wants XML, return them the list of people in XML format." (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
Supposing you have an action that adds a new person, optionally creating their company (by name) if it does not already exist, without web-services, it might look like this:
def create @company = Company.find_or_create_by_name(params[:company][:name]) @person = @company.people.create(params[:person]) redirect_to(person_list_url) end
Here’s the same action, with web-service support baked in:
def create company = params[:person].delete(:company) @company = Company.find_or_create_by_name(company[:name]) @person = @company.people.create(params[:person]) respond_to do |format| format.html { redirect_to(person_list_url) } format.js format.xml { render :xml => @person.to_xml(:include => @company) } end end
If the client wants HTML, we just redirect them back to the person list. If they want Javascript (wants.js), then it is an RJS request and we render the RJS template associated with this action. Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also include the personâs company in the rendered XML, so you get something like this:
<person> <id>...</id> ... <company> <id>...</id> <name>...</name> ... </company> </person>
Note, however, the extra bit at the top of that action:
company = params[:person].delete(:company) @company = Company.find_or_create_by_name(company[:name])
This is because the incoming XML document (if a web-service request is in process) can only contain a single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
person[name]=...&person[company][name]=...&...
And, like this (xml-encoded):
<person> <name>...</name> <company> <name>...</name> </company> </person>
In other words, we make the request so that it operates on a single entityâa person. Then, in the action, we extract the company data from the request, find or create the company, and then create the new person with the remaining data.
Note that you can define your own XML parameter parser which would allow you to describe multiple entities in a single request (i.e., by wrapping them all in a single root note), but if you just go with the flow and accept Rails' defaults, life will be much easier.
If you need to use a MIME type which isn’t supported by default, you can register your own handlers in environment.rb as follows.
Mime::Type.register "image/jpg", :jpg
How to test different responses in controller tests/specs
When you want to write a controller test or spec (rspec) to test out a different response type other than html, just set the HTTP_ACCEPTS header like so before the request:
@request.env['HTTP_ACCEPT'] = "application/rss" post :create, :blog => {}
Types array shorthand
You can have respond_to blocks that look like this:
respond_to do |format| format.html format.xml end
Here each individual format doesn’t receive a block and so Rails automatically tries to render the appropriate view for the mime type (e.g. action.html.erb, action.xml.erb or as a last resort action.erb)
You can do exactly the same thing by passing an array of Mime types to respond_to like this:
respond_to(:html, :xml)
Rails defined Mime Types
Here are all the default Rails Mime Types:
"*/*" => :all "text/plain" => :text "text/html" => :html "application/xhtml+xml" => :html "text/javascript" => :js "application/javascript" => :js "application/x-javascript" => :js "text/calendar" => :ics "text/csv" => :csv "application/xml" => :xml "text/xml" => :xml "application/x-xml" => :xml "text/yaml" => :yaml "application/x-yaml" => :yaml "application/rss+xml" => :rss "application/atom+xml" => :atom "application/json" => :json "text/x-json" => :json
How to test different responses of respond_to
You can shorten this:
@request.env['HTTP_ACCEPT'] = "application/rss"
To this:
@request.accept = "application/rss"
Also, if you send more than one Mime type, it will render the first one:
@request.accept = "text/javascript, text/html" #=> renders JS @request.accept = "text/html, text/javascript" #=> renders HTML
Security issue with non-HTML formats
Please note that using default to_xml or to_json methods can lead to security holes, as these method expose all attributes of your model by default, including salt, crypted_password, permissions, status or whatever you might have.
You might want to override these methods in your models, e.g.:
def to_xml super( :only => [ :login, :first_name, :last_name ] ) end
Or consider not using responds_to at all, if you only want to provide HTML.
Ordering of format blocks is important
The order in which your format blocks appear, like:
format.html { } format.js { }
are used to infer priority in cases where the appropriate format is ambiguous.
A catch-all format
If you’d like to specify a respond_to only for 1 or a few formats and render something else for all other formats, eg: (action.rss returns a feed but action.html or action.js should just render 404), use format.all:
respond_to do |format| format.rss { render_rss } format.all { render_404 } end
Rails will render an empty string for all formats that don’t specify a response explicitly.
Custom MIME Type
After you register a custom Mime::Type like stated above, you can do:
respond_to do |format| # .jpg corresponds to the second argument passed to #register # Mime::Type.register "image/jpg", :jpg format.jpg { ...do something here... } end
Re: How to test different responses
In addition to using:
@request.accept = "text/javascript" #=> request JS
as rubymaverick and nachocab suggested, you can also pass :format when calling your action, eg:
it "GET #most_viewed renders #most_viewed.js.rjs template if js requested" do get :most_viewed, :format => 'js' response.should render_template('most_viewed') end
You must use the yielded object
A warning note (at least for v2.3.4): if you don’t use the yielded format object, you will get a cryptic error like this:
NoMethodError (You have a nil object when you didn't expect it! The error occurred while evaluating nil.call): app/controllers/comments_controller.rb:11:in `create'
So make sure you use it!
include respond_to in your controller action only if there are multiple formats of that view
consider this
def index @users=User.get_users respond_to do |format| format.html format.json format.js end end
is good if you have a call to users/index by both
<%= link_to ("Show users",user_path)%> ##will render users/index.html.erb ===Also same call but with ajax <%= link_to ("Show users",user_path,remote=>true)%> ##will render users/index.js.erb..handled by respond_to block ===Also same call but with json <%= link_to ("Show users",user_path,remote=>true,:format=>json)%> ##will render users/index.json.erb..handled by respond_to block.
But if you have just first one,so remove respond_to block as you are sure that you only need index.html.erb ALWAYS
def index @users=User.get_users end
So if your action is being called both by BOTH ajax,non-ajax call,its good to use respond_to.
Rendering After Exception In respond_to() Block
Remember, format blocks set the response’s content type. This can present problems when handling errors.
class MediaController rescue_from ActionController::MissingFile do |e| # User's browser probably wont display this # Content-Type is application/x-shockwave-flash render :file => File.join(Rails.public_path, '404.html'), :status => 404 end # show details or stream video def show @media = Media.find params[:id] respond_to do |format| format.html format.flv { send_file @media.path, :disposition => 'inline' } end end end
For these situations you must set :content_type when calling render:
render :file => File.join(Rails.public_path, '404.html'), :status => 404, :content_type => 'text/html'