PDA

View Full Version : REST DELETE 405 HTTP response



Phoenicoperus
2 Dec 2013, 7:11 PM
Hi, I'm currently implementing a simple REST CRUD ExtJS example, but found a problem with DELETE HTTP method when used together with ExtJS rest proxy.

I'm trying to test all CRUD operations with an Ext.grid.Panel, for this I use store with a rest proxy. GET, POST and UPDATE works as expected but DELETE has a very wired behaviour, it works the first request but fail all consecutive requests.

The requests are exactly the same, I compared them line by line, they have the same headers and meta, same URL (with a different valid id on the path) but only the first one works, once I request a delete all subsequent requests return 405 HTTP Method not allowed.

I've been able to narrow the problem with a separated REST client and found that when passing a payload on DELETE (like ExtJS rest proxy does) it will behave in this way, only the first DELETE request works, but when I leave the payload empty, I can DELETE resources without any issue.

So my hypothesis is my Glassfish Web Server and JAX-RS/Jersey REST implementation dislikes having a payload on a DELETE request (which sounds quite logic since having a payload on a DELETE request makes no good use of REST standards)

Can you prevent ExtJS rest proxy from sending a payload on DELETE (maby extending rest/http proxy)? or if not can you modify Glassfish to allow/ignore having a payload on DELETE?

(Sencha, should have deletePayload: false on 4.2.2 =P~)

Here are the CODE examples of the explained behaviour:

Client ExtJS Store Code:


Ext.define('Application.store.Items', { extend: 'Ext.data.Store',
model: 'Application.model.Item',
autoLoad: true,
autoSync: true,
proxy: {
type: 'rest',
url: 'webresources/model.itementity',
noCache: false,
pageParam: false,
startParam: false,
limitParam: false,
reader: {
type: 'json',
successProperty: 'success',
totalProperty: "total",
root: 'data'
},
writer: {
type: 'json',
writeAllFields: true,
encode: false,
allowSingle: false,
root: 'data'
},
listeners: {
exception: function(proxy, response, operation) {
Ext.MessageBox.show({
title: 'WHY DO U HATE ME SERVER?',
msg: operation.getError(),
icon: Ext.MessageBox.ERROR,
buttons: Ext.Msg.OK
});
}
}
}
});


Client Extjs View Grid Remove:


handler: function() {
var selection = me.getSelectionModel().getSelection();


if (selection) {
store.remove(selection);
}
}


Server Jersey Code:


@DELETE
@Path("{id}")
@Consumes({"application/json"})
@Produces({"application/json"})
public ExtDataReturn remove(@PathParam("id") Long id) {
super.remove(super.find(id));


List<ItemEntity> data = new ArrayList<ItemEntity>();
ExtDataReturn extReturn = new ExtDataReturn(String.valueOf(data.size()), data, true);


return extReturn;
}


HTML DELETE Request and Response:

Request1/Response1:


Request URL:http://localhost:8080/ExtREST/webresources/model.itementity/4801
Request Method:DELETE
Status Code:200 OK


Request Headers:


Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:es,en-GB;q=0.8,en;q=0.6
Connection:keep-alive
Content-Length:62
Content-Type:application/json
Cookie:JSESSIONID=6261de69a5568dc68364fe6e0595; treeForm_tree-hi=treeForm:tree:configurations:default-config:sysProps
Host:localhost:8080
Origin:http://localhost:8080
Referer:http://localhost:8080/ExtREST/
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
X-Requested-With:XMLHttpRequest


Request Payload:
{"data":[{"id":4801,"name":"test1","email":"[email protected]"}]}


Response Headers:


Content-Length:38
Content-Type:application/json
Date:Tue, 03 Dec 2013 02:52:11 GMT
Server:GlassFish Server Open Source Edition 4.0
X-Powered-By:Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition 4.0 Java/Oracle Corporation/1.7)

Response Payload:
{"data":[],"success":true,"total":"0"}


Request2/Response2:


Request URL:http://localhost:8080/ExtREST/webresources/model.itementity/4802
Request Method:DELETE
Status Code:405 Method Not Allowed


Request Headers:


Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:es,en-GB;q=0.8,en;q=0.6
Connection:keep-alive
Content-Length:62
Content-Type:application/json
Cookie:JSESSIONID=6261de69a5568dc68364fe6e0595; treeForm_tree-hi=treeForm:tree:configurations:default-config:sysProps
Host:localhost:8080
Origin:http://localhost:8080
Referer:http://localhost:8080/ExtREST/
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
X-Requested-With:XMLHttpRequest


Request Payload:


{"data":[{"id":4802,"name":"test2","email":"[email protected]"}]}


Response Headers:


Allow:GET,DELETE,OPTIONS,PUT
Content-Language:es
Content-Length:1151
Content-Type:text/html;charset=ISO-8859-1
Date:Tue, 03 Dec 2013 02:58:58 GMT
Server:GlassFish Server Open Source Edition 4.0
X-Powered-By:Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition 4.0 Java/Oracle Corporation/1.7)

scottmartin
3 Dec 2013, 1:27 PM
What does your model look like? I would think that only ID would be sent on the URL

Phoenicoperus
3 Dec 2013, 2:49 PM
Here is the item model



Ext.define('Application.model.Item', {
extend: 'Ext.data.Model',
fields: [{
name: 'id',
type: 'int',
useNull: true
}, 'name', 'email'],
//Important! Validations are needed so ADD wont insert empty in database
validations: [{
type: 'length',
field: 'name',
min: 1
}, {
type: 'length',
field: 'email',
min: 1
}]
});


I don't believe its model related issue, since as I mentioned before I tested it with an external REST client sending a DELETE request with the following payload and I was able to replicate the problem.


{"data":[{"name":"test","email":"[email protected]"}]}
(this is the same payload that ExtJS rest proxy produces)

So what I'm really looking for, is how to prevent ExtJS rest proxy from sending the json entity as a payload on DELETE requests to the server or to configure Glassfish with Jersey to accept/ignore payloads on DELETE requests.

My idea is to create a custom implementation of a rest proxy extending ExtJS http ajax proxy, but I'm a bit lost in how to create it, and how to implement the correct REST standards, with its corresponding events listeners preferable for the HTTP response code and not for a successProperty.

An example of this would be great, and would certainly help a lot of people in the ExtJS community.

The custom true rest proxy should target the following standards:

Create:
Method: POST
URL: /webresources/item
Request: json payload without id (since database will provide it).
Response: json payload with id (the id generated by database).
Rsponse code: 201 (created).

Read:
Method: GET
URL: /webresources/item or /webresource/item/123 (with id for an specific entry or without id for a complete list)
Request: no payload.
Response: json payload with item or items.
Rsponse code: 200 (ok).

Update:
Method: PUT
URL: /webresources/item/123 (with 123 being the entry id)
Request: json payload with the fields to modify.
Response: no payload or a payload with success: true depending on ExtJS flavour.
Response code: 204 (no content) or 200 (ok) depending if content was returned or not.

Delete:
Update:
Method: DELETE
URL: /webresources/item/123 (with 123 being the entry id)
Request: no payload.
Response: no payload or a payload with success: true depending on ExtJS flavour.
Response code: 204 (no content) or 200 (ok) depending if content was returned or not.

hope this encourage someone to help me, because I'm willing to share this with all the community.

skirtle
3 Dec 2013, 9:41 PM
Take a look at the doRequest method of Ext.data.proxy.Ajax. It looks like the key is the check of operation.allowWrite(), which will cause a payload for everything except a read request.

Subclassing Operation isn't really an option so you'll either have to patch the Operation class itself or pull a sleight-of-hand on each instance, maybe in the proxy's destroy method.

e.g.:


Ext.define('MyOperationPatch', {
override: 'Ext.data.Operation',

allowWrite: function() {
return this.action !== 'read' && this.action !== 'destroy';
}
});

Or:


Ext.define('MyProxy', {
alias: 'proxy.my-proxy',
extend: 'Ext.data.proxy.Rest',

destroy: function(operation) {
operation.allowWrite = Ext.emptyFn;

this.callParent(arguments);
}
});

For more general advice on writing a custom proxy see:

http://skirtlesden.com/articles/custom-proxies

Phoenicoperus
3 Dec 2013, 10:04 PM
Thank you so much, you made someone really happy today Skirtle:



Ext.define('MyProxy', {
alias: 'proxy.my-proxy',
extend: 'Ext.data.proxy.Rest',

destroy: function(operation) {
operation.allowWrite = Ext.emptyFn; this.callParent(arguments);
}

});


This is exactly the idea I had in mind, but I didn't knew how to do it. Extending the proxy and overriding the destroy function. I tested you snippet and it and works like a charm.

Anyway, I still want to write a better REST proxy, and your article http://skirtlesden.com/articles/custom-proxies is awesome. It's a very good place to start, once again THANK YOU SO MUCH SKIRTLE =D>!

RodBiffi
15 Apr 2014, 4:45 AM
I implemented the Ext.ux.grid.Search by Aaron Snyder and it loads the extraParams of the grid's proxy. These extraParams end up in the buildRequest method, which indeed bypass the solution above mentioned (verified by the presence of "Content-Length" in the DELETE request header).

My first approach was to manually clear the extraParams before the store.sync() (batch destroy) and restore them in the callback, so my grid remains filtered after the operations.