Content negotiation not working with respond_to stanza - ruby

Good afternoon Stack Overflow,
def show
# Translating ID to event
#event = Event.find(params[:id])
respond_to do |format|
format.html
format.json { render :json => #event }
end
end
This code should (according to what I read online) generate an html response when the Accept header is set to text/html, and JSON whenever the header is set to application/json. Yet whenever I test this with Postman I always end up getting the representation of the resource that is set first.
In the above code that would be html, even when the accept header is set solely to Json. If I switch them around, I get a json representation, indifferent of what I set the header.
Side question: I'm kinda new to Ruby, and would like to fully understand what the do |x| do_a() do_b() syntax/stanza is. Is this like a Switch statement or more a lambda?

Solution found
My brain just gave out for 2 days, because apparently I was sending Content-Type headers in a GET request, instead of the appropriate Accept Header.
Thx #yoones for trying to help me :)

Related

Accept file upload (without a form) in Sinatra

I have this Sinatra::Base code:
class Crush < Sinatra::Base
post '/upload' do
erb params.inspect
end
end
I am using Postman and its interface for uploading a file. So I send a POST request with form-data, where in the body of the request the name is hello and the value is a file test.txt which contains just a simple string hey there.
When I do params.inspect I get this long string
{"------WebKitFormBoundaryocOEEr26iZGSe75n\r\nContent-Disposition: form-data; name"=>"\"hello\"; filename=\"test.txt\"\r\nContent-Type: text/plain\r\n\r\nhey there\r\n------WebKitFormBoundaryocOEEr26iZGSe75n--\r\n"}
So basically a long has with a single key and a single value. Reading most Sinatra tutorials (where the file is accepted from a form), there's a nice way Sinatra handles this using params[:file], but this doesn't seem to be the case when the file is coming straight from the body of an HTTP request.
I tried a non-modular approach too withou Sinatra::Base, thinking it's some parsing middle-ware missing, but got the same result.
Is there something I'm missing here? Must I go and make my own custom parser to get the content of this long hash? Or is there an easier way?
I figured it's Postman issue. When I switch from 'x-www-form-urlencoded' to 'form-data' in Postman, in the Header section, the field: Content-Type => application/x-www-form-urlencoded is NOT removed. So for those who encounter this problem, make sure you remove it manually.

Supporting multiple JSON content types in Grape

I’ve been using Grape to write an Evernote-like API, and have been using Collection+JSON (MIME type "application/vnd.collection+json"). I now want to also support JSON API (mime type "application/vnd.api+json").
(The reason I want to do this is so it will be easier to write an Ember client, since Ember Data has support for JSON API built in. It might make more sense to write client-side JavaScript to solve my problem, but I’m stronger in Ruby than JavaScript.)
My goal is to have the server support either format, and choose which one based on the Accept header. Is this a reasonable thing to do, or does it not make sense to have multiple JSON types? It appears that Grape doesn’t support this. If I just have the wrong idea, then the code below is probably not relevant to that answer.
Here are some relevant pieces of my server code:
class NotesServer < Grape::API
content_type :json, ‘application/json’
content_type :json_api, 'application/vnd.api+json'
formatter :json_api, lambda { |object, env| object.to_json_api }
content_type :collection_json, 'application/vnd.collection+json'
formatter :collection_json, lambda { |object, env| object.to_collection_json }
resource :notes do
desc 'Get a note.'
params do
requires :id, type: Integer, desc: 'Note ID.'
end
route_param :id do
get do
NoteRepresenter.new Note.find(params[:id])
end
end
end
…where NoteRepresenter defines both to_json_api and to_collection_json.
The idea is to use the same Representer in either case, but to call the appropriate method depending on the Accept header.
When I run curl against this with any Accept header, the response has Content-Type: application/json, and “to_json” is called on the representer. If I leave out the content_type :json, ‘application/json’ line, I get a 406 Not Acceptable response every time.
I figured out that in the Grape library, in lib/grape/middleware/formatter.rb, the "format" of a request is considered JSON for any of these MIME types, since the vnd.collection+ or vnd.api+ parts are parsed out of the header. So my question is, is this a matter of Grape not supporting what I’m trying to do, or am I trying to do something that makes no sense?
I found a reasonable way to do this that I’mr reasonably happy with, but I don’t think it totally aligns with how Grape expects you to do things.
I created a custom formatter called JsonFormatter, so my server code looks like this:
class NotesServer < Grape::API
formatter :json, JsonFormatter
# …
end
And the formatter itself:
module JsonFormatter
def self.call(object, env)
case env['HTTP_ACCEPT']
when 'application/vnd.api+json'
object.to_json_api
when 'application/vnd.collection+json'
object.to_collection_json
else
object.to_json
end
end
end
So now either to_json_api or to_collection_api (or to_json) will be called on the presenter object, depending on the Accept header. One issue I haven’t solved yet is that the Content-Type is set to application/json in all cases.

Controller can not detect ajax requests

I am using simple_form gem and generating the form I am specifying the remote:true option like this:
<%= simple_form_for #webinar, validate: true, remote:true do |f| %>
So, the output html for the form is the following fragment:
<form accept-charset="UTF-8" action="/webinars" class="simple_form new_webinar" data-remote="true" data-validate="true" enctype="multipart/form-data" id="new_webinar" method="post" novalidate="novalidate"> ... </form>
As I checked, using the standard form_for helper is adding the data-remote='true' to the form when remote:true options is used. And as you can see from the generated html, when I am using the simple_form gem there is such attribute, too.
So, in my controller I have:
def create
#webinar = Webinar.new(params[:webinar])
respond_to do |format|
if #webinar.save
format.html { redirect_to #webinar, notice: 'Webinar was successfully created.' }
format.js
format.json { render json: #webinar, status: :created, location: #webinar }
else
format.html { render action: "new" }
format.json { render json: #webinar.errors, status: :unprocessable_entity }
end
end
end
But, always the format.html is used. What i am doing wrong?
EDIT:
I have used logger.debug request.format to check what is the actual format ask for and in the log file it was:
text/html
So, the issue must be in the simple_form generated form - what can be wrong there when we have "data-remote=true"?
You're confusing format.json|format.html with remote: true. Both are different. The presence of remote: true does not imply format.json.
format.json does not indicate that the URL was invoked via javascript. It only means that the client expects JSON output. i.e. it does not indicate where input came from, it indicates what output is required.
The general use of remote:true is, instead of reloading the page, you submit the form as an Ajax request and then show the response in a JQuery popup or something. But if you want to display the response as a JQuery popup - you need HTML output, not JSON output right?
Some people use remote: true to load HTML content in a popup. Your use case is to do remote: true but you're expecting JSON formatted data. Rails cannot make these decisions for you. It by default sends the request to /webinars and expects that you will handle the HTML response. If you really want JSON response - then customize the URL to which the Ajax request is posted:
simple_form_for #webinar, url: webinar_path(format: :json), .....
If you do the above, now the webinar controller will be called with JSON format.
Overall:
remote:true can be used with both format.html and format.json
The majority use case in a Rails application is to handle remote: true as a controller request as usual, render a partial HTML template (i.e. the response content alone without the overall page layout/navigation/etc) and send it back as HTML to be displayed in a popup
Most people just blanket-handle remote callbacks and display a JQuery popup. So they don't need to write individual code for each remote forms
So by default, Rails calls format.html for remote requests
If you specifically want format.json, and if you really want to handle the JSON manually on the client, change the URL accordingly. But this is not the majority use case
Making an Ajax request does not mean the content-type is JSON. It is a HTML request made using Javascript. e.g. In jquery $.ajax method, check these two options: accept and dataType. If you really want to send Accepts: application/json header, then you have to manually specify that when making the Ajax request (or you need to end the url with .json if its a Rails app).
So even if you make a normal Ajax request to /webinars, like $.ajax('/webinars', ...) - it won't go to format.json ! It will still only go to format.html. If you really want a JSON format, then you must say $.ajax('/webinars', { accepts: 'application/json' }), or you must say $.ajax('/webinars.json')
Edit: Minor clarification
It seems like you are not directly requesting a JSON formatted document in the generated form action. Perhaps one option is to set :format to json in your routes file using this technique: http://guides.rubyonrails.org/routing.html#defining-defaults
You can also define other defaults in a route by supplying a hash for the :defaults option. This even applies to parameters that you do not specify as dynamic segments. For example:
match 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
Rails would match photos/12 to the show action of PhotosController, and set params[:format] to "jpg".
I can not believe that I have lost so much time in trying to understand why the simple_form is not working as expected.
Finally, it appears that I have done everything in the right way and the issue was caused because:
AJAX can not be used for file uploads.
In order to solve my problem I simply have to add the following gem into my Gemfile and run the bundle install command:
gem 'remotipart', '~> 1.0'
Then add the following line in my application.js file:
//= require jquery.remotipart
More information about:
remotipart gem
How to upload multiple files using jQuery

How to read POST data in rack request

When I run the curl command
curl -v -H "Content-type: application/json" -X POST -d '{"name":"abc", "id":"12", "subject":"my subject"}' http://localhost:9292
to send a POST request with data to my Rack application, my code prints out {}. That is coming from puts req.POST() in the code below.
Why does it print out {} instead of the POST data? And how do I correctly access the POST data in my Rack application?
require 'json'
class Greeter
def call(env)
req = Rack::Request.new(env)
if req.post?
puts req.POST()
end
[200, {"Content-Type" => "application/json"}, [{x:"Hello World!"}.to_json]]
end
end
run Greeter.new
From reading the docs for POST, looks like it is giving you parsed data based on other content types. If you want to process "application/json", you probably need to
JSON.parse( req.body.read )
instead. To check this, try
puts req.body.read
where you currently have puts req.POST.
req.body is an I/O object, not a string. See the body documentation and view the source. You can see that this is in fact the same as mudasobwa's answer.
Note that other code in a Rack application may expect to read the same I/O, such as the param parsers in Sinatra or Rails. To ensure that they see the same data and not get an error, you may want to call req.body.rewind, possibly both before and after reading the request body in your code. However, if you are in such a situation, you might instead consider whether your framework has options to process JSON directly via some option on the controller or request content-type handler declaration etc - most likely there will be an option to handle this kind of request within the framework.
Try:
env['rack.input'].read
I found it in "How to receive a JSON object with Rack" and, though it still sounds weird to me, it likely works.
You can try:
req.params
Hope this can help you.

Why am I getting a 405 on a POST request for JSON?

So I've got a Padrino app with a controller that looks roughly like this (simplified for brevity):
App.controllers :questions do
get :index, :with => :id, :provides => [:html, :json] do
# Stuff
end
post :index, :with => :id, :provides => [:html, :json] do
# Other stuff
end
end
If I hit "questions/1" in my browser, I see the HTML page for the given question. If I hit "questions/1.json", I see the JSON representation of the question. Exactly like I'd expect.
Now, if I submit a POST request to "questions/1" via a web form, it works. But if I try to send a POST request to "questions/1.json" (signaling that I want the response in JSON format—or at least that's how I thought it worked), I get a 405 Method Not Allowed.
I'm guessing there's something basic I'm misunderstanding here. What am I missing? More importantly, how should I define a route to accept POST requests and provide either HTML or JSON responses?
Well, I'm not really sure why this was happening; but for now I've gotten around the issue by setting the "ACCEPT" header in my POST request to "application/json" instead of tacking ".json" onto the end of the URL (and upon my limited internet research, this may be the preferred approach anyway).

Resources