Skip to content

Instantly share code, notes, and snippets.

@technoweenie
Created October 3, 2012 11:21
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save technoweenie/1b6ba8757e8166d1f0ce to your computer and use it in GitHub Desktop.
Save technoweenie/1b6ba8757e8166d1f0ce to your computer and use it in GitHub Desktop.
Hypermedia question

People have been talking about including hypermedia with REST Resources, but there are very few public examples that implement all of it. One common technique is to add *_url attributes:

{ "id": 1
, "self_url": "/issues/1"
, "comments_url": "/issues/1/comments"
}

This only gives you room to put a URL. Because of this, I've been leaning towards HAL:

{ "id": 1
, "_links":
  { "self": {"href": "/issues/1"}
  , "comments": {"href": "/issues/1/comments"}
  , "close": {"href": "/issues/1/close", "method": "post"}
  }
}

Should hypermedia tell you when you have permissions to access those resources?

// admin permissions
{ "id": 1
, "_links":
  { "self": {"href": "/issues/1", "method": "get,patch,delete"}
  , "close": {"href": "/issues/1/close", "method": "post"}
  }
}

// read-only permissions, no access to close/update issues
{ "id": 1
, "_links":
  { "self": {"href": "/issues/1", "method": "get"}
  }
}

The HAL spec doesn't mention method properties at all. But, I think letting a client know the difference between an Issue I can edit or close is very useful. Is it any better if I have a separate relation for each action?

{ "id": 1
, "_links":
  { "self": {"href": "/issues/1", "method": "get"}
    "edit": {"href": "/issues/1", "method": "patch" }
    "delete": {"href": "/issues/1", "method": "delete" }
  , "close": {"href": "/issues/1/close", "method": "post"}
  }
}

Finally, is the "close" relation even appropriate? Or should I assume clients know they can close Issues by setting "state" to "closed" (which is how the GitHub Issues API works)?

@technoweenie
Copy link
Author

@steveklabnik: Ah yea, I guess I didn't get that far into the RFC. I really like HAL's simplicity. Apparently the same href can have multiple relations though:

Link: <http://example.org/>;
             rel="start http://example.net/relation/other"

@kevinswiber
Copy link

@technoweenie: Siren solves for multiple link relations, as well, by making the link rel an array. It can be more verbose than HAL, but that's all part of the flexibility/usability sliding scale.

@technoweenie
Copy link
Author

Thanks for the comments everyone. I think I'm settling on something like this:

{ "id": 1
, "_links":
  { "self": {"href": "/issues/1", "method": "get"}
    "edit": {"href": "/issues/1", "method": "patch" }
    "delete": {"href": "/issues/1", "method": "delete" }
  , "close": {"href": "/issues/1/close", "method": "post"}
  }
}

I can easily omit "method" on known relations (self, edit, etc), and add URIs describing the relations as desired. I guess if I have problems, I can version the media type and transition to a new hypermedia format :)

@kevburnsjr
Copy link

The important questions I see here are :

  1. Can it be useful to specify a method on a link? I would say yes.
  2. Is a relation a composite of a resource identifier and a method? I would say no.

Consider the following:

GET /order/123/item/34

{
    some: "data",
    _links: {
        "self" : { href: "/order/123/item/34", method: "GET" },
        "update" : { href: "/order/123/item/34", method: "PUT" },
        "delete" : { href: "/order/123/item/34", method: "DELETE" },
        "order" : { href: "/order/123" }
    }
}

Shouldn't those resource relationships for create, update, etc all be rel="self"? Wouldn't it make more sense to write :

GET /order/123/item/34

{
    some: "data",
    _links: {
        "self" : { href: "/order/123/item/34", methods: [ "GET", "PUT", "DELETE" ] },
        "order" : { href: "/order/123", methods: ["GET", "DELETE"] }
    }
}

In answer to your question, I would say that the 'close' relation and url are not appropriate except as a GET to a resource returning a representation containing a form which could be used to issue an unsafe request to some other resource such as the issue itself or a subordinate resource such as status. So as a shortcut, status makes more sense:

GET /issue/123

{
    some: "data",
    _links: {
        "self" : { href: "/issue/123" },
        "status" : { href: "/issue/123/status", methods: ["PUT"], type: "text/plain", options: ["open", "closed"] }
    }
}

Which might elicit a client request such as:

> PUT /issue/123/status
> content-type: text/plain
> 
> closed

@mikekelly
Copy link

@technoweenie personally, I think if you suspect some clients may need to act dynamically based on their permissions for a particular resource, it is better to expose those permissions as an additional related resource and to couch it in "domain terms" rather than raw http. e.g.

{ "id": 1
, "_links":
  { "self": {"href": "/issues/1" }
  , "permissions": { "href": "/issues/1/permissions" }
  , "edit": {"href": "/issues/1" }
  , "delete": {"href": "/issues/1" }
  , "close": {"href": "/issues/1/close"}
  }
}

where /issues/1/permissions is a resource that is only privately cacheable, and varies based on who is making the request. Its state should tell a given client what permissions it has on the issue at a given point in time.

This approach isolates the non-cacheable state in a separate resource that will only be accessed by the clients that require it, thus extending the cache potential TTL for the issue resource itself.

@mikekelly
Copy link

fiww, if a methods property on a link is considered a design approach that many people are keen on I am happy to look at including it in the specification.

so far I have been deliberately stingy with adding features in order to keep the complexity of the type down to the bear minimum - however most of us consider certain features a basic need in a hypermedia API then lets look at adding them. The easiest way to do that is to start a thread on the discussion group, so if you can spare the time please do.

@steveklabnik
Copy link

@technoweenie

Apparently the same href can have multiple relations though:

Yep!

@mikeschinkel
Copy link

@technoweenie - I really like what you've settled one, it would be my choice. And @mikekelly's suggestion for a private permissions resource makes a lot of sense too.

@mikekelly, good idea to be stingy, but +1 for adding "method."

@technoweenie
Copy link
Author

@mikekelly: A separate permissions resource is not a great option. Think of a mobile app rendering a list of resources. Without per-resource hints, the app has to either call that URL for each resource, or assume you have access to edit each resource. This is a real problem on the GitHub Android app, actually. But, I also understand how this complicates caching.

@kevburnsjr: I like the methods array. That was my original inclination (though I opted for comma separated values vs a JSON array).

@mikeschinkel
Copy link

@technoweenie - The method array looses the different named actions. I think there's more value in having the explicit named actions and decoupling the link objects; the client can always aggregate them if needed.

@glennblock
Copy link

The downside of using the key for the rel is you can't represent collections with a similar rel. For example imagine a rel of "query" where I could have multiple queries. I prefer having a collection of links with REL and HREF explicitly specified. Or alternatively, not using rel at all and having first class elements within my media type. The values for those elements could be arrays if multiple were supported as in the Query case.

@glennblock
Copy link

A few other thoughts.

  • Embedding the method. Why not use options? The disadvantage is that the list may only accurate for a point in time, i.e. my permissions may have changed. I guess I can see the simplicity argument as a consumer can just read the payload and know what to use.
  • If the key is REL it should probably still be qualified with a namespace so as not to conflict with other RELs that is unless it is something globally registered like "EDIT".
  • Having granular links like "Edit", "Delete" I think is fine. One could arge if you have that you don't need to embed method as the media type docs could say what method you use.

@mikekelly
Copy link

@technoweenie ok if that is a concern you have a couple of options:

  • have the app assume lowest level of permissions until the relevant resource permissions are fetched.
  • make the permissions an _embedded resource. You can avoid this impacting caching by using a mechanism like ESI. I've been planning on adding this to hal+json. Something like:
200 OK
X-Edge-Include: true

{
  ...
  "_embedded": {
    "permissions": {
       "href": "/issues/1/permissions",
       "edge-include": true
    }
  }
  ...
}

which should be caught by an edge proxy and replaced with the contents of /issues/1/permissions.

If neither of those are attractive to you and you still want to trade-away cacheability, then you might still want to favour keeping the links consistent and expressing the permissions as part of the issue representation (varying by user), i.e:

{ "id": 1
, "_links":
  { "self": {"href": "/issues/1" }
  , "edit": {"href": "/issues/1" }
  , "delete": {"href": "/issues/1" }
  , "close": {"href": "/issues/1/close"}
  }
, "permissions": {
    "can": ["commit"]
  , "cannot": ["administer"]
  }
}

@andreineculau
Copy link

I read through the comments, and I didn't see anyone touch on the freshness of the permission to use method M (see CORS). Sorry if I missed it.

Oct 3 - I make a request and get back information that I can edit resource R
Oct 4 (after some time window, or after many other requests that changed the state of resources) - I try to edit R and fail

If you do the same, but use OPTIONS instead:
Oct 3 - same, just that you only get a link to the resource R - what you can do with it remains unknown
Oct 4 - OPTIONS R gives you back GET (no edit), and thus you cannot edit R, but you can read R

I personally like the granularity that OPTIONS gives you. I can refresh the permission for a resource R1, without refreshing the representation of another resource R2 that links to resource R1.

@LouisStAmour
Copy link

I agree with @andreineculau, though I'm not as convinced that OPTIONS is a necessity (it's an option, har har)

I think this needs to be handled using a smarter client. After all, given REST's preference for stateless server communication, you'd want to have a background poll ask for OPTIONS and update the display or perform another action (save a draft?) before a user clicks a button and tries to edit. I'm reminded of the traffic-less session sharing specified in OpenID Connect, where you can timeout a session on another domain in another tab by having support for smarter clients polling cross-domain cookies.

That all said, what's wrong with trying and failing to Edit, if a client is graceful enough to recover from errors without data/intention loss? Or, it's a pain, but if permissions are given as part of a session object, then cached as a session credential or OAuth key would be, well, it's not the responsibility of the object you want to edit to tell you whether or not you can edit it some time indeterminately in the future.

Perhaps the OAuth key is revoked or the session expires? Or perhaps you load something when not signed in, then sign in and want to perform actions against that object without reloading it? It seems easier to leave permissions granting and cues for such to the application to request separately as it needs to as part of maintaining session state or OAuth.

It could be a smarter layer of application- or domain-object inferred functionality (to hide or show certain actions) on top of a much simpler stateless structure (where all actions are offered regardless of state).

A question I would pose, with no right or wrong answer, is what about an API that's primarily read-only but to which admins have write access? Should read-only clients see actions they cannot perform as of moment? Despite potential confusion, I would argue yes, as we often see actions on the web which we are not allowed (e.g. commenting) that only prompt us for authentication after we show interest. In addition, it's likely better for understanding an API to reveal all actions at all times, as despite the added complexity, you can call the API under any circumstance and understand the totality of the API at any given moment, without having multiple accounts with differing permissions to view edge case actions.

As to some of the JSON I've seen in this GIST, I really feel like the more we go down this route and the uglier the JSON gets, the more I wonder if we should just embed simple JSON objects with an XML wrapper for navigation/context and be done with this nonsense. Play to the strengths of each, and put simple data in JSON and more complex data in XML. Though at that point you have to parse both, so maybe it's not as ideal either. Hmm...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment