A RESTless week
I spent about a week of my time at Acquia on improving Drupal 8’s REST support.
That time was spent fixing, reviewing, triaging and documenting.
Drupal 8’s REST works very well, we just have to make it more friendly & helpful, remove Drupalisms and support more REST capabilities.
Fixing, reviewing & triaging
I went through the entire issue queues of rest.module
, serialization.module
and hal.module
. I was able to mark about a dozen bug reports as duplicates, fix a dozen or so support requests, have reviewed probably a few dozen patches, rerolled about a dozen patches, created at least another dozen patches and … triaged 100% of the open issues. I clarified titles of >30 issues.
Now the rest.module
issue queue (the most important one) fits on a single page again!1 I collaborated a lot with neclimdul, klausi, damiankloip, dawehner and others.
dawehner and I decided to tag the issues that were especially relevant using the RX (REST Experience)
issue tag.
I felt it was important to get a comprehensive picture of Drupal 8’s REST support state, so I insisted on going through all open issues (and was given the time to do so). This enabled me to document the current state of things (and upcoming improvements).
Documenting
So, I spent several days doing nothing else but writing and improving documentation, just like I did for the modules and subsystems I co-maintain. The following drupal.org
handbook pages have either received minor updates, received complete overhauls or were written from scratch:
- d.o/documentation/modules/serialization
- d.o/developing/api/8/serialization
- d.o/documentation/modules/rest
- d.o/developing/api/8/rest
- d.o/documentation/modules/hal
- d.o/documentation/modules/basic_auth
- d.o/developing/api/8/authentication
- d.o/documentation/modules/rest/start
- d.o/documentation/modules/rest/get
- d.o/documentation/modules/rest/post
- d.o/documentation/modules/rest/patch
- d.o/documentation/modules/rest/delete
So, there you go, documentation covering fundamentals, like that for the Serialization API:
- Serializing & deserializing
- Using the
serializer
service’s (\Symfony\Component\Serializer\SerializerInterface
)serialize()
anddeserialize()
methods:$output = $serializer->serialize($entity, 'json'); $entity = $serializer->deserialize($output, \Drupal\node\Entity\Node::class, 'json');
- Serialization format encoding/decoding (format → array → format
- The encoder (
\Symfony\Component\Serializer\Encoder\EncoderInterface
) and decoder (\Symfony\Component\Serializer\Encoder\DecoderInterface
, to add support for encoding to new serialization formats (i.e. for reading data) and decoding from them (i.e. for writing data).- Normalization (array → object → array)
- The normalizer (
\Symfony\Component\Serializer\Normalizer\NormalizerInterface
) and denormalizer (\Symfony\Component\Serializer\Normalizer\DenormalizerInterface
), to add support for normalizing to a new normalization format. The default format is as close to a 1:1 mapping of the object data as possible, but other formats may want to omit e.g. local IDs (for example node IDs are local, UUIDs are global) or add additional metadata (such as URIs linking to related data).- Entity resolvers
- In a Drupal context, usually it will be (content) entities that end up being serialized. When given an entity to normalize (object → array) and then encode (array → format), that entity may have references to other entities. Those references may use either UUIDs (
\Drupal\serialization\EntityResolver\UuidResolver
) or local IDs (\Drupal\serialization\EntityResolver\TargetIdResolver
). For advanced use cases, additional mechanisms for referring to other entities may exist; in that case, you would add an additional entity resolver.
… to more practical information, such as the Getting started: REST configuration & REST request fundamentals
handbook page for the rest
module:
1. Configuration
First read
RESTful Web Services API — Practical.Now you know how to:
- expose data as REST resources
- grant the necessary permissions
- customize a REST resource’s formats (JSON, XML, HAL+JSON, CSV …)
- customize a REST resource’s authentication mechanisms (cookie, OAuth, OAuth 2.0 Token Bearer, HTTP Basic Authentication …)
Armed with that knowledge, you can configure a Drupal 8 site to expose data to precisely match your needs.
2. REST request fundamentals
2.1 Safe vs. unsafe methods
REST uses HTTP, and uses the HTTP verbs. The HTTP verbs (also called
request methods) are:GET
,HEAD
,POST
,PUT
,DELETE
,TRACE
,OPTIONS
,CONNECT
andPATCH
.
Some of these methods are safe: they are read-only. Hence they can never cause harm to the stored data, because they can’t manipulate them. The safe methods areHEAD
,GET
,OPTIONS
andTRACE
.
All other methods are unsafe, because they perform writes, and can hence manipulate stored data.Note:
PUT
is not supported for good reasons.2.2 Unsafe methods & CSRF protection:
X-CSRF-Token
request headerDrupal 8 protects its REST resources from CSRF attacks by requiring a
X-CSRF-Token
request header to be sent when using a non-safe method. So, when performing non-read-only requests, that token is required.
Such a token can be retrieved at/rest/session/token
.2.3 Format
When performing REST requests, you must inform Drupal about the serialization format you are using (even if only one is supported for a given REST resource). So:
- Always specify the
?_format
query argument, e.g.http://example.com/node/1?_format=json
.- When sending a request body containing data in that format, specify the
Content-Type
request header. This is the case forPOST
andPATCH
.Note:
Accept
-header based content negotiation was removed from Drupal 8 because browsers and proxies had poor support for it.3. Next
Now you’re ready to look at concrete examples, which start on the next page.
If that particular handbook page had already existed, it would have saved me so much time! The next page then contains examples for how to do GET
requests, using various tools:
cURL
curl http://example.com/node/1?_format=hal_json
Guzzle
$response = \Drupal::httpClient() ->get('http://example.com/node/1?_format=hal_json', [ 'auth' => ['username', 'password'], ]); $json_string = (string) $response->getBody();
jQuery
jQuery.ajax({ url: 'http://example.com/node/1?_format=hal_json', method: 'GET', success: function (comment) { console.log(comment); } });
… and the following pages then provide concrete examples (in those same tools) for POST
, PATCH
and DELETE
requests2.
Enjoy!
Why I did all of the above
It took me about three days to successfully PATCH
a Comment
entity3.
Why days?
I first forgot to specify the Content-Type
request header. Then it turned out I also forgot the X-CSRF-Token
request header — which was not documented anywhere to be a thing. I eventually found out about that Drupal-specific request header by analyzing the REST PATCH
test coverage. Why did I not find it sooner? Because Drupal 8 was giving utterly unhelpful, and actually downright nonsensical (and incorrect!) responses4. It doesn’t end there though. Turns out that if you try to update an entity using JSON (and not HAL+JSON, which works fine), you MUST specify the bundle
(otherwise it’s impossible to denormalize the entity you’re sending), but you also MUST NOT specify the entity type’s bundle if it’s a Comment
(because you’re not allowed to modify this by CommentAccessControlHandler
). So … it literally was impossible to update a comment5!
I didn’t have any experience with/knowledge about Drupal 8’s REST API. But I’m deeply familiar with Drupal 8. And it still took me days. Of course I wanted to prevent anyone from ever having to go through that.
Today, anybody would start at d.o/documentation/modules/rest/start and then look at d.o/documentation/modules/rest/patch and would hence be able to avoid all these pitfalls. Soon, Drupal will provide more helpful responses4 and allow comments to be updated5 using JSON.
-
Once the “fixed” issues disappear. 50 issues fit on a single page. I didn’t count the number of issues before I started, but it was at least 70, and I think ~90. ↩︎
-
These handbook pages already existed mostly, but lacked clarity, coherence and completeness. That’s what I tried to add. ↩︎
-
We’re working on an experiment in progressive decoupling. For that to work, you of course need solid REST support. Once those experiments become worth sharing, we will. ↩︎
-
I’m fixing that in https://www.drupal.org/node/2659070. ↩︎ ↩︎
-
I’m fixing that in https://www.drupal.org/node/2631774. ↩︎ ↩︎
Comments
Thanks a lot for all the documentation updates, Wim. Much appreciated!
off topic - what are you using for your footnotes?
Markdown. https://drupal.org/project/markdown
“I wanted to prevent anyone from ever having to go through that” - and I can attest that your efforts paid off. Nice work! It is very evident in the docs, as well!
Excellent job writing clear, concise and dead straight-forward background, explanations and instructions. No small feat!
I (unfortunately) did not find your blog post in time.
I do not know Drupal very well, and I had to create a custom RestResource in it. Every problem you had, I had. In the same order.
The Content-Type and CSRF token being required without being documented anywhere (I finally found out about it in some tutorial)
And I’m still struggling with the final problem: posting regular JSON to my custom POST resource.
All I get is
Could not denormalize object of type , no supporting normalizer found
, even though I don’t want it to normalize anything. It’s just a damn object.It’s just a damn object in JavaScript, but Drupal needs to be able to map it to the right fields, and those fields have a very specific data structure. See the https://www.drupal.org/developing/api/8/serialization.
section atRelationships between entities (one of Drupal’s outstanding distinctions) seem particularly fraught in POSTs even though GETs in my experience have worked flawlessly. Happy to help expand documentation on d.o. assuming I can find a way forward. Any recommendations on cracking this piece of the puzzle?
When using the default normalization and JSON encoding, that hasn’t been tricky at all. I suspect you’re referring to the
application/hal+json
format?This is very valuable information, thanks! Do we have some documentation somewhere (or tips) on the best approach to upgrade from say D6 to D8 with existing services (from the services module). The problem is that there’s a lot of external applications (iOS, Android, Other) consuming that API, so existing paths/routes should be the same when we launch the D8 version of the site. However, with
_format=json
argument and theX-CSRF-Token
for unsafe requests, this might become a little tricky. Although I could probably just swap everything out anyway as I’ll have to define my own resources to match the existing requests, nothing much I can use for which is core. Sorry for the late night rambling :)I don’t know of any such documentation. Would be a good blog post or — better yet — a good Drupal.org handbook page :)