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)?

@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