Rails respond_with -- why does POST return a URL instead of the data?
Asked Answered
H

3

20

This is a question "why does it work this way", not "how do I make this work".

My app is calling a third party REST API that returns JSON, and returning the result as part of my own JSON API.

I was using the Rails 3 respond_to and respond_with methods; in the case of GET requests, this works as I expect, just passing through the JSON.

In the case of POST, it does more, including making a URL from the object returned to pass in a :location option. But since my object is just JSON (not ActiveRecord), I get an error.

For example...

# POST /api/products.json with params id=:id
def create
  query_string = "#{user_id}&id=#{params[:id]}"
  @products = third_party_api_wrapper.products(query_string, 'POST')
  respond_with @products
end 

My wrapper for the 3rd party API makes a POST request, which comes back fine, then Rails returns a 500 error which is logged like this:

NoMethodError (undefined method `{"response":{"message":"product 4e1712d9ec0f257c510013f8 selected"}}_url' for #<MyController> 

Rails want's my @products object to know how to make a location URL.

CLARIFICATION: The @products object returned by the third party API is pure JSON -- a string, which you can see embedded in the error log message above. This error is occurring because Rails seems to want the object to be something more -- in the Rails internal API support, it is an ActiveRecord object.

If I replace the new respond_with with sytax with the old-style

respond_to do |format|
  format.json { render :json => @products }  # note, no :location or :status options
end

then everything works. And this is what I have done, so I don't have a "how" problem, instead have a "why" question.

Ryan Daigle's post on the introduction explains that what's happening is expected.

My question is: why does respond_with expect anything other than data (and the HTTP status?), and apparently just for POST.

I am not saying it's wrong, just trying to understand the rationale for the Rails implementation.

Hahn answered 19/6, 2012 at 20:25 Comment(3)
I'm not sure what you are asking/saying here. Can you clarify? Are you... (1) asking why respond_with did not work for you? (2) saying the 3rd party API you are using does not return "just" data and a status code? (3) asking "why should an API return anything other than data ?"Heraldry
If your main question is "why should an API return anything other than data (and the HTTP status?). I am not saying it's wrong, just trying to understand the rationale." perhaps it would help to give us more specifics about the API.Heraldry
Please output @products and tell us the result.Heraldry
H
17

Summary: Rails gets its rationale from HTTP and REST.

(Thanks for your updated question. Now I understand your core question: "I am not saying it's wrong, just trying to understand the rationale for the Rails implementation.")

Now for the explanation. The rationale for how Rails behaves is rooted in embracing HTTP and REST conventions.

Just to bridge from what you've read to what I'm about to elaborate on, I want to mention the relevant parts from Ryan Daigle's article on Default RESTful Rendering:

If the :html format was requested:

[some text removed]

  • [after PUT or POST and no validation errors] redirect to the resource location (i.e. user_url)

(The text [in brackets] was added by me.)

If another format was requested, (i.e. :xml or :json)

[some text removed]

  • If it was a POST request, invoke the :to_format method on the resource and send that back with the :created status and the :location of the new created resource"

Let me put this in my words about what Rails believes is good practice:

  • For human content (e.g. HTML), after a POST or PUT, the server should tell the browser to redirect via a 303 to the newly created resource. This is common practice -- a very useful thing because a user wants to see the updates resulting from their edits.

  • For machine content (e.g. JSON, XML), after a PUT, the server should just render a 201. The client, in this case, a program consuming an API, might decide to stop there. (After all, the client specified the request and got a 201, so all is honky dory.) This is why 201 (success), not 303 (redirect), is used. If the client wants to request the newly created resource, it can look it up using the Location header -- but a redirect should not be forced.

In either case note that the location of the newly created resource is required. This is why @products in your example above needs to contain both the data and the location.

For background, I've shared a little from the W3C Page on 201 Created:

10.2.2 201 Created

The request has been fulfilled and resulted in a new resource being created. The newly created resource can be referenced by the URI(s) returned in the entity of the response, with the most specific URI for the resource given by a Location header field. The response SHOULD include an entity containing a list of resource characteristics and location(s) from which the user or user agent can choose the one most appropriate. The entity format is specified by the media type given in the Content-Type header field. The origin server MUST create the resource before returning the 201 status code. If the action cannot be carried out immediately, the server SHOULD respond with 202 (Accepted) response instead.

I hope this helps to explain the rationale. It is my (naive?) understanding that this rationale is well accepted across Web frameworks. Historically, I suspect that Rails was the fervent implementation-ground (new word alert!) for many fervent supporters of REST and the Resource Oriented Architecture.

Heraldry answered 21/6, 2012 at 4:46 Comment(2)
Ah, now I understand the rationale. Yeah, it makes sense that once you have created something, you would want to have a reference to it. I am not sure, but I think the respond_with implementation assumes that the object's to_s method will return a Rails-route-compatible name (I'll inspect a Rails object and find out when I get a minute) such that it can do something with location. This works fine when you're in a Rails-centric world, but not in my case where all I want from the API is data, and am handling navigation in a way that no API could reasonably predict. Thanks for the help!Hahn
For human content, redirecting also force the browser to do a GET request to display the result which will prevent the user from re-POSTing his data if doing a browser refresh.Swartz
B
2

The 'why' has been answered excellently by @david-james. This is just a short 'how' to answer via respond_with:

class Api::V1::UsersController < ApplicationController

  respond_to :json

  def create
    @user = User.create(...)
    respond_with @user, location: url_for([:api, :v1, @user])
  end

end
Bork answered 30/11, 2014 at 23:55 Comment(0)
H
1

To answer this question: "why should an API return anything other than data (and the HTTP status?). I am not saying it's wrong, just trying to understand the rationale."

I can think of no good rationale. More importantly, I can't see any way that an API could return anything except a data structure! (This question doesn't make sense to me!)

By definition, an API call must return a data structure. (It might be as simple as a string. It might be JSON. It might be XML.) It can use content negotiation to decide on the format. It may or may not be a strict schema, but at the very least a client library must be able to parse it. In any case, the API documentation should make this abundantly clear and stick to it. How else can client libraries expect to interoperate?

I think I'm missing the point here, this seems too obvious. (I suspect you are having another problem in your code above.)

Heraldry answered 19/6, 2012 at 20:39 Comment(3)
thanks for all of your replies. I have tried to clarify the question to make it clear that I understand what is happening when respond_with is called. And I have a solution. It just doesn't make sense to me that Rails should work this way, so I want to understand more.Hahn
Thanks for clarifying. I'm going to keep this answer intact for now. I answered your clarified question separately.Heraldry
@DavidJames, you stated: "By definition, an API call must return a data structure". Not so. The RFC spec you quote in your answer for a 201 response says "SHOULD" return, not "MUST return, an entity. There are some use cases where a 201 without a body is acceptable (although some would argue to use a 204 response for that so it is explicit there is no body). My point is an API call does not always have to return a data structure. Interesting discussion here: codeschool.com/discuss/t/…Jacquelinejacquelyn

© 2022 - 2024 — McMap. All rights reserved.