In the previous episode AngularJS was deployed for some front-end asynchronous calls. Typically a browser allows GET and POST requests for resources back to the server the website originates from, but not from an entirely different domain. This principle is based on the so-called same-origin policy. It is not a bug but a safety feature. This policy can be bypassed though with cross-origin resource sharing (CORS). In Angular mode it allows a direct request for information directly from the front-end. Web Services with CORS enabled are difficult to spot but the people at the Chemical Translation Service have one. In one of their services you can send an InChi key which is a code attached to a molecule and have synonyms for that molecule returned. An Angular service can be defined like this:


chemicalApp.factory('SynonymData', function($http) {
    return {
        getSynonym: function(inchi, callback) {
            $http.get('http://cts.fiehnlab.ucdavis.edu/service/synonyms/' + inchi).success(callback);
        }
    };
});

Here is the request:


GET /service/synonyms/LFQSCWFLJHTTHZ-UHFFFAOYSA-N HTTP/1.1
Host: cts.fiehnlab.ucdavis.edu
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0 FirePHP/0.7.4
Accept: application/json, text/plain, */*
Accept-Language: nl,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
DNT: 1
Referer: http://zf2project.localhost/chemistry
Origin: http://zf2project.localhost
x-insight: activate
Connection: keep-alive

and here is the response:


HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Server: Jetty(7.6.9.v20130131)

The key is in the response header: Access-Control-Allow-Origin: * which basically is telling the browser that it is okay the data were requested like this.

The response itself is a json string containing a list of synonyms for the molecule in question , which happens to be ethanol. That many?


[
  "Ethyl Alcohol & Water, 80%",
  "silent spirit",
  "Alcohol, dehydrated",
  "Ethanol Fixative 80% v/v",
  "Thanol",
  .........
]

But how can this be accomplished? Time to make our own CORS-aware REST webservice! A temperature-scale conversion app is just the thing the internet is in need of. Reminding everyone that this demo was raised on a Zend Framework 2 platform and that the Chemical module is a vendor module, these are the required steps, starting with the REST server:

In the module config define a new route, a new controller and a new view strategy:


'may_terminate' => true,
		'child_routes' => array(
		    'rest' => array(
			'type' => 'segment',
			'options' => array(
			    'route' => '/rest/kelvin[/:id]',
			    'defaults' => array(
				'controller' => 'rest',
				'action' => ''

				),
			    'constraints' => array(
				'id' => '\d{0,4}'
			    )
			),
		    ),
		),
...

'invokables' => array(	  
	    'Rest'
	    => 'Chemical\Controller\RestController'
	),

...

 'view_manager' => array(
	...
	'strategies' => array(
	    'ViewJsonStrategy',
	),
...

Add a new controller that extends AbstractRestfulController and introduce a get() method


public function get($id)
    {
	$headers = array(
	    'Access-Control-Allow-Origin' => '*'
	);	
	$this->getResponse()->getHeaders()->addHeaders($headers);
	return new JsonModel(array("data" => 
array(array('scale' => 'Kelvin', 'value' => $id), array('scale' => 'Celsius', 'value' => ($id - 273.15)))));
    }

With a GET request like this:


chemicalApp.factory('TemperatureData', function($http) {
    return {
        getTemperature: function(kelvin, callback) {
            $http.get('http://orinoco.vander-lingen.nl/chemistry/rest/kelvin/' + kelvin).success(callback);
        }
    };
});

the response is a json string containing temperature conversion data.

Basically what happens is that the onDispatch method in the abstract controller examines the request made and then based on the method (get, post etc) invokes the get() method. When the original request was made via a GET then all it takes is to add the additional “Access-Control-Allow-Origin” header. Without it the request is still made but the response is empty.

But now the same request is made via a POST:


chemicalApp.factory('TemperatureData', function($http) {
    return {
        getTemperature: function(kelvin, callback) {
            $http.post('http://orinoco.vander-lingen.nl/chemistry/rest/kelvin/200', {kelvin:kelvin}).success(callback);
        }
    };
});

Now the request is all of a sudden handled as an OPTIONS and not as a POST (what you might expect) and no data are returned. The reason: the browser makes a so-called preflight to the server to enquire if it is allowed to make the actual POST request. The remedy is to define an options method and return three distinct headers:


 public function options() {
$headers = array(
	    'Access-Control-Allow-Origin' => '*',
	    'Access-Control-Allow-Methods' => 'GET, POST, PUT',
	    'Access-Control-Allow-Headers' => 'content-type'
	);
	$this->getResponse()->getHeaders()->addHeaders($headers);
    }

The POST request is then handled by a create() method rather than a get() method. Note that the router action should be empty!

The complete exchange now looks like this with as OPTIONS request:


OPTIONS /chemistry/rest/kelvin/200 HTTP/1.1
Host: orinoco.vander-lingen.nl
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0 FirePHP/0.7.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: nl,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
DNT: 1
Origin: http://zf2project.localhost
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

and server response:


HTTP/1.1 200 OK
Server: Apache/2
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: content-type
Vary: Accept-Encoding,User-Agent
Content-Encoding: gzip
Content-Length: 20

and finally the POST response:


HTTP/1.1 200 OK
Server: Apache/2
Access-Control-Allow-Origin: *
Vary: Accept-Encoding,User-Agent
Content-Encoding: gzip
Content-Length: 78
Keep-Alive: timeout=1, max=99
Connection: Keep-Alive
Content-Type: application/json; charset=utf-8

Browsers do not enforce CORS. In this demo, CORS is enabled by default in AngularJS. By contrast in the jQuery ajax() method it takes setting a custom request header to trigger the preflight.

A demo is available here. The code is available at Github. Note that the demo site is same-origin and will collect data via a simple POST. The CORS implementation was tested via the local website connecting to the external website.

Advertisements