Blog
All Blog Posts | Next Post | Previous PostCORS and preflighted requests with TMS XData
Friday, April 10, 2015
From Wikipedia: Cross-origin resource sharing (CORS) is a mechanism that enables many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain from which the resource originated. In other words, if you are trying to access entities in a TMS XData server from a web page (for example, using AJAX requests), this mechanism (CORS) will eventually get in action.
Updated July 15, 2019. Enabling CORS in TMS XData is very simple, just add TCORSmiddleware (declared in unit Sparkle.Middleware.Cors) to your XData module:
uses {...}, Sparkle.Middleware.Cors; {...} XDataServerModule.AddMiddleware(TCorsMiddleware.Create);
Or, if you are using the TXDataServer component at design-time, just right-click it, choose "Manage middleware list...", click the button to add a new middleware (or right-click the blank middleware list and choose "Add middleware...") and then choose the CORS middleware from the list.
And it will handle most of what's needed, including preflighted requests. This post could end here if you are looking about how to deal with CORS in TMS XData. But let me use the opportunity to explain and illustrate how CORS works.
Let's use the SQLiteConsolerServer demo that is included in TMS XData distribution. This very simple demo creates an SQLite database in memory, fill it with some predefined data (artists, albums and tracks), and starts a XData server in the address "http://localhost:2001/tms/music" to provide the objects from the database. This is what you will get after running the demo.
Now if we go to our browser (I'm using Chrome here) and enter the URL "http://localhost:2001/tms/music/Track(1)", this is what we get:
So far so good, our server is providing the JSON representation of our Track object with id equals to 1. But what happens if we try to do the same request using AJAX? Let's create a small HTML page with some JavaScript that performs the same GET request we're doing directly with the browser. Here is the full code of the HTML:
<!DOCTYPE html> <html> <body> <script> function processResponse(xmlhttp) { switch(xmlhttp.status) { case 200: var track = JSON.parse(xmlhttp.responseText); document.getElementById("getButton").innerText=track.Name; break; case 404: document.getElementById("getButton").innerText="(not found)"; break; default: document.getElementById("getButton").innerText="(invalid)"; } } function getTrack1Name() { var xmlhttp=new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4) { processResponse(xmlhttp); } } xmlhttp.open("GET","http://localhost:2001/tms/music/Track(1)",true); xmlhttp.send(null); } function deleteTrack1() { var xmlhttp=new XMLHttpRequest(); xmlhttp.open("DELETE","http://localhost:2001/tms/music/Track(1)",true) xmlhttp.send(null); } </script> <button onclick="getTrack1Name();" id="getButton">Get Track 1 Name</button> <br><br> <button onclick="deleteTrack1();">Delete Track 1</button> </body> </html>
Code is very simple, it just provides two buttons that perform GET and DELETE requests to get the name of Track 1 and delete Track 1, respectively.
Let's open that page in browser (I'm using a WAMP server here but you could just double-click the HTML file):
If we click the first button to retrieve the name of Track 1, we get this:
It doesn't work. Why is that? If we press F12 in Chrome to get more info about it, you can get a clue about what's going on:
That's CORS in action. The browser doesn't allow a request from domain "localhost:8080" (where our web page is located) to the domain "localhost:2001" (where our XData server is located) unless our server states that it allows it (using the mentioned response header).
We can then modify our SQLiteConsoleServer demo to add that small line of code mentioned in the beginning of this post:
{...} Module.AccessControlAllowOrigin := '*'; // Add this line Server.AddModule(Module); Server.Start; {...}
Then if we restart our server, refresh our test page, and try pressing the button again, here is what we get:
Now it works! Here is the response returned by the XData server:
HTTP/1.1 200 OK Content-Length: 228 Content-Type: application/json Server: Microsoft-HTTPAPI/2.0 access-control-allow-origin: * Date: Fri, 10 Apr 2015 14:08:03 GMT { "$id": 1, "@xdata.type": "XData.Default.Track", "Id": 1, "Name": "Black Dog", "Composer": "Jimmy Page, Robert Plant, John Paul Jones", "Milliseconds": 296672, "Genre@xdata.ref": "Genre(1)" }
Note the presence of header "access-control-allow-origin" which states that the server allows requests from any server. You could just restrict this to a specific server origin address by simply defining the name of the server instead of using "*" when setting the property.
Now what about preflighted requests? It will happen when we click our "Delete Track 1" button. From this nice Mozilla web page explaining CORS, it explains that a request must be preflighted if the HTTP method is different than GET, HEAD or POST, or even if request use custom headers or content-type different than some accepted ones. This covers a lot of very common REST requests: DELETE, PUT, or POSTing JSON data.
So what happens exactly when we click "Delete Track 1" button? This is the request Chrome will send to our XData server:
OPTIONS http://localhost:2001/tms/music/Track(1) HTTP/1.1 Host: localhost:2001 Connection: keep-alive Access-Control-Request-Method: DELETE Origin: http://localhost:8080 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36 Accept: */* Referer: http://localhost:8080/tests/cors.html Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8,pt;q=0.6
Note that instead of sending a DELETE request, it sent an OPTIONS request, which is the preflighted one. This means the browser is "checking" the server if the request he's going to perform is valid. It indicates it's going to perform a DELETE method using the "Access-Control-Request-Method" header. If the request had different headers, it would also send header "Access-Control-Request-Headers" to check with the server if the headers will be allowed.
The XData server then responds informing the client that the DELETE request will be accepted:
HTTP/1.1 200 OK Server: Microsoft-HTTPAPI/2.0 access-control-allow-methods: GET,PUT,PATCH,DELETE,OPTIONS access-control-allow-origin: * access-control-max-age: 1728000 Date: Fri, 10 Apr 2015 14:16:15 GMT Connection: close Content-Length: 0
And finally, Chrome performs the actual DELETE request:
DELETE http://localhost:2001/tms/music/Track(1) HTTP/1.1 Host: localhost:2001 Connection: keep-alive Origin: http://localhost:8080 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36 Accept: */* Referer: http://localhost:8080/tests/cors.html Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8,pt;q=0.6
If we press the "Get Track 1 Name" button again, we will be informed it doesn't exist:
So, although enabling CORS in XData is just a single line of code, my intention here was to explain CORS with little more details, including preflighted requests, and show how XData makes it work under the hood.
Wagner Landgraf
This blog post has received 4 comments.
Wagner Landgraf
Dernegi Cansuyu
Wagner R. Landgraf
All Blog Posts | Next Post | Previous Post
Thank you
ghazali