How does rails determine incoming request format? - ruby-on-rails

I'm just wondering how rails knows the format of the request as to correctly enter in the famous:
respond_to do |format|
format.html
format.xml
format.json
end
As an example consider this situation I have faced up. Suppose that via javascript (using jQuery) I make a POST request expliciting dataType: json
$.ajax({
type: 'POST',
url: 'example.com',
data: data,
dataType: 'json'
});
When this request reach controller action, standing inside it with ruby debugger, I inspect #request.format and I can see that content-type is application/json. Then the controller respond to json format as expected.
But I'm confused with the format symbol especified in the routes. Suppose that a request is made to example.com/parts.json but in the request the content type is application/html or application/xml. Is the controller responding to json format or html or xml??
Thanks!

From ActionController::MimeResponds: "Rails determines the desired response format from the HTTP Accept header submitted by the client."

The incoming Content-Type only affects the way the request is parsed. It doesn't affect the response format.
Since Rails 5.0, the response format is determined by checking for:
A format parameter (e.g. /url?format=xml)
The HTTP Accept header (e.g. Accept: application/json)
The path extension (e.g. /url.html)
You can see this in the implementation of ActionDispatch::Http::MimeNegotation#formats. Here is an excerpt from Rails v6.1:
if params_readable?
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
elsif extension_format = format_from_path_extension
[extension_format]
elsif xhr?
[Mime[:js]]
else
[Mime[:html]]
end

Related

Turbolinks to Turbo upgrade has broken form redirection

I'm attempting to upgrade to Turbo from Turbolinks and I've found that the client is not rendering redirects for form submissions.
Versions:
rails 6.1.4
hotwire-rails 0.1.2
#hotwired/turbo-rails 7.0.0-beta.8
I've ignored the incompatibility between Turbo and Devise for now - just trying to get regular forms working without having to disable Turbo on them.
Here's an example action:
def update
authorize #label
#label.update(label_params)
if #label.save
redirect_to document_labels_path(document_id: #document.id)
else
render :new, status: :unprocessable_entity
end
end
Here's a rendered form:
<form class="simple_form new_label" id="label_form" novalidate="novalidate" action="/documents/72/labels" accept-charset="UTF-8" method="post">
...
</form>
When submitting a valid form, the server will say Processing by LabelsController#create as TURBO_STREAM and correctly serve a 302. It will then serve the 200 for the redirect location. The browser however is left just looking at the submitted form. Changing the redirect status to 303 doesn't change anything.
I added a console.log for every Turbo event:
document.addEventListener("turbo:load", function () {
console.log('TURBO:LOAD')
})
document.addEventListener("turbo:click", function () {
console.log('TURBO:CLICK')
})
document.addEventListener("turbo:before-visit", function () {
console.log('TURBO:BEFORE-VISIT')
})
document.addEventListener("turbo:visit", function () {
console.log('TURBO:VISIT')
})
document.addEventListener("turbo:submit-start", function () {
console.log('TURBO:SUBMIT-START')
})
document.addEventListener("turbo:before-fetch-request", function () {
console.log('TURBO:BEFORE-FETCH-REQUEST')
})
document.addEventListener("turbo:before-fetch-response", function () {
console.log('TURBO:BEFORE-FETCH-RESPONSE')
})
document.addEventListener("turbo:submit-end", function (event) {
console.log('TURBO:SUBMIT-END')
// event.detail
})
document.addEventListener("turbo:before-cache", function () {
console.log('TURBO:BEFORE-CACHE')
})
document.addEventListener("turbo:before-stream-render", function () {
console.log('TURBO:BEFORE-STREAM-RENDER')
})
document.addEventListener("turbo:render", function () {
console.log('TURBO:RENDER')
})
This is what the output is for a successful form submission:
TURBO:BEFORE-FETCH-REQUEST
TURBO:SUBMIT-START
TURBO:BEFORE-FETCH-RESPONSE
TURBO:SUBMIT-END
There is no render event. Investigating event.detail.fetchResponse.response for turbo:submit-end it seems to be perfectly aware that the client should redirect, it just didn't.
Response {type: "basic", url: "http://lvh.me:3000/documents/72/labels", redirected: true, status: 200, ok: true, …}
body: (...)
bodyUsed: true
headers: Headers {}
ok: true
redirected: true
status: 200
statusText: "OK"
type: "basic"
url: "http://lvh.me:3000/documents/72/labels"
__proto__: Response
Update: It is actually performing the redirect and the server is generating the response. The issue is that the client is not rendering the redirect response.
What is happening here is that your application is specifying that it prefers turbo-stream responses over text/html responses. If you were to look at your request headers for the redirect page, you'll likely see the following:
Accept: text/vnd.turbo-stream.html, text/html, application/xhtml+xml
As a result, Rails returns the data with the first type it recognizes, which is text/vnd.turbo-stream.html. Turbo in your browser sees this and, since it's not interpretable as a Turbo Stream, unhelpfully ignores it quietly.
The solution (workaround?) is to make sure you are redirecting to the html version of your page:
redirect_to document_labels_path(document_id: #document.id, format: :html)
This will return the page with a Content-Type of text/html, and Turbo will replace the whole page with the contents.
Jeff's answer is correct but I wanted to share the specific fix for the issue I was having.
If you use HAML or Slim, I've seen it on more than one codebase where developers rename all template files .haml instead of .html.haml (same for Slim). It's never bitten me before using Turbo, but without .html in the filename, part of Rails won't know what format to serve a response in, so it defaults to the request format.
Turbo makes a turbostream request when submitting a form, but if the response is a redirect, it expects it to be text/html in order to render it. If it receives a turbostream response to a redirect request, Turbo just sits there doing nothing with no console errors or warnings (terrible default behavior IMO).
So if your templates do not include .html, just add it back and Turbo will render redirects. You may still need status: :see_other.
More information:
https://github.com/hotwired/turbo-rails/issues/122
https://github.com/hotwired/turbo-rails/issues/287
Adding to the excellent answer of Jeff Seifert.
If you don't need turbo streams, you may also unregister the turbo-stream content type altogether by putting this into an initializer e.g. config/initializers/turbo.rb:
Rails.application.config.after_initialize do
Mime::Type.unregister(:turbo_stream)
end

ajax call to database in rails

I'm trying to get rows that match a var, In my case a url. in the database and return the whole row as a json format.
Basically if url in table1 matches the url under eventurl in table2. Then the whole row is passed through to the ajax request as a jsonformat.
Heres what i have so far.
Routes.rb
resources :gig do
scope constraints: { format: "json" } do
get :gigdata, on: :member
end
end
In my ajax call i have this
url: 'gigdata/' + gigurlofevent , (no need to include the whole url ajax file here as its working elsewhere)
and in my controller i have this
respond_to :json, only: :gigdata
def gigdata
gig = Gigstable.where(eventurl: (params[:gigurlofevent]))
render json: gig
end
Now at the moment, I can't get into the gigdata with a byebug.
I'm wondering what i need to do/ what ive missed
Thanks
Sam
edit
Heres the start of the ajax call down the the success function
$.ajax({
beforeSend: function(xhr) {
xhr.setRequestHeader("Content-Type", "application/json");
},
type: 'GET',
crossDomain: true,
dataType: 'json',
url: 'gigdata/' + gigurlofevent ,
success: function(json) {
debugger;
Common problem:
You wrote:
(no need to include the whole url ajax file here as its working
elsewhere)
Actually you need to include complete code because common problem lays in Content-Type field. You need to specify content type of request in header like 'application/json'.
I am testing my backend api with curl
For example:
curl http://localhost/api/v1/some_action -H 'Content-Type: application/json' -vvv
Last parameter -vvv very useful to debug requests because it makes curl to output request and response with headers in console.
Another way to debug ajax
Open developer tools (for chrome it's: F12 or ctrl+shift+i), click on network tab and click on XHR filter button. Reload page with ctrl+r and execute your ajax request one more time. Information about your ajax request will appear in the window below filters. Now you can check out what kind of data comes from server and whats going wrong.
Golden rule
Rails server outputs all requests in console. Don't be shy to read output when something works not like you expected.

How can I have Grape return error messages in CSV format?

I have a Rails app and I have implemented api using Grape gem. Now, I created a custom error formatter (CSVFormatter) to return error response in CSV format.
And, also I have this in my application's v2.rb file:
error_formatter :csv, Api::Base::Errors::CSVFormatter
When I hit a url like this:
http://example.com/api/v2/datasets/CODE/data.csv?&trim_start=06/01/99&trim_end=2014-05/28&sort_order=desc
It shows the error in the console like this which is good and means that my custom error formatter is working properly:
Error
trim_start is invalid
trim_end is invalid
But, I just need to download this error message in a csv file. After looking at Grape's documentation, I found a way of setting Content-type and I tried this:
rack = Rack::Response.new(as_csv , 422, { "Content-type" => "text/csv" }).finish
rack[2].body[0]
But, this is not working as I expected.
EDIT:
Looks like there is no clean way of doing it using grape without forcefully overriding the status code according to the answer of Simon. But, one may not wish to do that as it may result other issues in the application like if some other program tries to read the data from the api and gets the incorrect response or so even without knowing why.
You're looking for the Content-Disposition header. Include it in your response like this:
Content-Disposition: attachment; filename=error.csv
And the Web browser will treat the response body as a file to be downloaded (to "error.csv", in this example).
However, modifying your code to do this is complicated by two things:
From the Grape source code it's apparent there's no way to set response headers from within an error formatter, so you'll need to add a custom exception handler that formats the response body and sets the response headers appropriately for each output format you plan to support.
According to my experimentation, browsers will ignore the Content-Disposition header if the HTTP status code indicates an error (e.g. anything in the 400 or 500 range), so the status code will also need to be overridden when the user requests a CSV file.
Try adding this to your API class:
# Handle all exceptions with an error response appropriate to the requested
# output format
rescue_from :all do |e|
# Edit this hash to override the HTTP response status for specific output
# formats
FORMAT_SPECIFIC_STATUS = {
:csv => 200
}
# Edit this hash to add custom headers specific to each output format
FORMAT_SPECIFIC_HEADERS = {
:csv => {
'Content-Disposition' => 'attachment; filename=error.csv'
}
}
# Get the output format requested by the user
format = env['api.format']
# Set the HTTP status appropriately for the requested output format and
# the error type
status = FORMAT_SPECIFIC_STATUS[format] ||
(e.respond_to? :status) && e.status ||
500
# Set the HTTP headers appropriately for the requested format
headers = {
'Content-Type' => options[:content_types][format] || 'text/plain'
}.merge(FORMAT_SPECIFIC_HEADERS[format] || { })
# Format the message body using the appropriate error formatter
error_formatter =
options[:error_formatters][format] || options[:default_error_formatter]
body = error_formatter.call(e.message, nil, options, env)
# Return the error response to the client in the correct format
# with the correct HTTP headers for that format
Rack::Response.new(body, status, headers).finish
end
Now if you configure your API class to handle two different formats (I've picked CSV and plain-text here for simplicity), like this:
module Errors
module CSVErrorFormatter
def self.call(message, backtrace, options, env)
as_csv = "CSV formatter:" + "\n"
message.split(",").each do |msg|
as_csv += msg + "\n"
end
# Note this method simply returns the response body
as_csv
end
end
module TextErrorFormatter
def self.call(message, backtrace, options, env)
as_txt = "Text formatter:" + "\n"
message.split(",").each do |msg|
as_txt += msg + "\n"
end
as_txt
end
end
end
content_type :csv, 'text/csv'
content_type :txt, 'text/plain'
error_formatter :csv, Api::Base::Errors::CSVErrorFormatter
error_formatter :txt, Api::Base::Errors::TextErrorFormatter
You should find your API always returns an error response suitable for the requested format, and triggers the browser to download the response only when CSV format is requested. Naturally this can be extended to support as many formats as you like, by explicitly declaring content types and error formatters.
Note there's one case in which this code doesn't automatically do the right thing, and that's when an error response is invoked directly using error!. In that case you'll have to supply the correct body and headers as part of the call itself. I'll leave extracting the relevant parts of the above code into reusable methods as an exercise for the reader.

Node.js PUT or POST request to Rails 3 Server

I'm trying to make a PUT and/or POST request from Node.js to my Rails 3 server. I am passing my parameters in the body but they are not getting turned into the params hash in the Rails controller. My code is as follows:
http = require('http');
request = require('request');
qs = require('qs');
data = qs.stringify({name: 'Aaron', points: 10});
request_opts = {
uri: 'http://localhost:3000/users/update',
method: 'PUT',
body: data
}
request(request_opts, function() { console.log(arguments) });
I put a debugger in my Rails controller and params is nil but response.body.read returns the string I have in data.
How can I make a PUT / POST request from Node.js where the parameters are compatible with Rails?
The querystring module will serialize your data as a parameter encoded form. By default rails expects a JSON serialized entity. You should use JSON.stringify instead of qs.stringify to serialize the your user into the request body. Alternatively, you can explicitly specify a content-type header of application/x-www-form-urlencoded to tell rails the type of the body you are sending.

Ruby Net::HTTP::Get and JSON responses

I'm trying to connect to an API and retrieve the json results with my rails app, however it doesn't seem to work.
Take for example:
#request = Net::HTTP::Get.new "http://example.com/?search=thing&format=json"
When I try the url in my browser it works! and I get JSON data, however when I try that in Ruby the body is nil.
>> y #request
--- !ruby/object:Net::HTTP::Get
body:
body_stream:
header:
accept:
- "*/*"
user-agent:
- Ruby
method: GET
path: http://example.com/?search=thing&format=json
request_has_body: false
response_has_body: true
Any thoughts?
I usually use open-uri
require 'open-uri'
content = open("http://your_url.com").read
`
You need to actually send the request and retrieve a response object, like this:
response = Net::HTTP.get_response("example.com","/?search=thing&format=json")
puts response.body //this must show the JSON contents
Regards!
PS: While using ruby's HTTP lib, this page has some useful examples.

Resources