The Grails 2.3 version introduced some nice REST improvements - it is now very easy to generate RESTful endpoints for your data model. I was also very happy to discover that handling business logic rules is nicely supported by overriding just a few core methods (e.g. queryForResource, listAllResources and so on - take a look at the RestfulController documentation).
So when I tried connecting this backend to the AngularJS $resource service, most of the things worked beautifully, with the exception of delete calls: a delete Ajax call would always get returned as a redirect request. This would make Firefox issue a prompt to the user if they really wanted to follow that redirect, which is the wrong behaviour and something the user should not have to decide about anyway.
What gives? Taking a look at the RestfulController source the relevant parts of the delete method are implemented like this:
request.withFormat { form { flash.message = message(code: 'default.deleted.message', args: [message(code: "${resourceClassName}.label".toString(), default: resourceClassName), instance.id]) redirect action:"index", method:"GET" } '*'{ render status: NO_CONTENT } // NO CONTENT STATUS CODE }
The first thing one should be careful about is that request.withFormat does not do the same thing as withFormat: the later parses either the Accept HTTP header, the ?format= parameter or the format= directive in the URL mappings while the former looks at the Content-Type header only. I lost a bit of time on this because I didn’t read the withFormat documentation carefully enough.. my bad.
So the issue was with AngularJS not setting the Content-Type header for DELETE requests and Grails interpreting the request as a HTTP form request for which a redirect is the most sensible action. This behaviour on Grails' part is a bit odd since DELETE requests aren’t supposed to contain any content and therefore don’t require Content-Type to be defined - on the other hand depending on the Accept header has problems of its own since browsers seem to send strange things in it. Fine, I’ll just define Content-Type in the AngularJS request and be done with it.
But it turns out that’s not so easy! There are several ways to set Content-Type in a $resource request like using a transformRequest function or a headers option (which is not documented clearly enough in my opinion). None of these work that simply! Why? There’s an issue recorded for AngularJS that gives us the answer: the gist of it is that some browsers do funny things to Content-Type in case of requests without a body, such as a DELETE request.
So the final solution was to send a small body with the delete request, like so:
_this.resource('..url..', { id: '@id'}, { remove: { method: "DELETE", isArray: false, headers: { 'Content-Type': 'application/json' }, data: {} } });
I’m hoping for smooth sailing after this - AngularJS and Grails' REST improvements seem like a match made in heaven :)
Thank you for this post. Very helpful indeed.
ReplyDeleteDid you contact the Grails dev or have you raised an issue to change the current behaviour of REST DELETE request? It would be extremly nice if the client code don't have to implement such a workaround.
I was in contact with a Grails developer while debugging the issue, so they are aware. I didn't raise a proper issue, though - as far as I understand most of these issues are caused by browsers sending weird headers in various scenarios so at the end of the day the above solution is probably the most robust and I don't think it breaks any standards, either.
ReplyDelete