Arquivo da tag: html

HTML5 em Ação

Nota do livro: 8.

Descrição rápida:
O HTML5 não constitui apenas algumas tags e recursos novos adicionados a um velho padrão – ele é a base da Web moderna, alavancando seus serviços interativos, UI de página única, jogos interativos e aplicativos empresariais complexos. Com o suporte ao desenvolvimento de aplicativos móveis baseados em padrões, recursos poderosos como o armazenamento local e WebSockets, ótimas APIs de áudio e vídeo e novas opções de layout com o uso de CSS3, SVG e Canvas, o HTML5 entrou em sua fase áurea.

O livro HTML5 em Ação fornece uma introdução completa ao desenvolvimento web com o uso de HTML5. Ele examina a especificação HTML5 por meio de códigos e exemplos do mundo real. Também faz jus ao termo “em Ação” disponibilizando o guia útil e prático necessário para a construção segura dos aplicativos e sites que você (e seus clientes) espera há anos.
Qual o conteúdo?

Novos elementos semânticos e tipos de entrada de formulário
Design de aplicativo de página única
Criação de elementos gráficos interativos
Aplicativos web móveis
Este livro é dedicado aos novos recursos HTML5 e supõe que você esteja familiarizado com HTML padrão.

CORS with Angular.js and Sinatra

https://devblast.com/b/cors-with-angular-js-and-sinatra

UPDATE: I’ve created a follow-up article that shows PUT and DELETE calls.

Today, I’m gonna talk about something that always caused me a lot of pain, everytime I had to deal with it : Same-origin policy. This thing is simply awful if you have to make HTTP requests from Javascript.

To counter that, you can use JSONP. But only to make GET requests. If you need more than that (POST, PUT, DELETE), you will have to use Cross-Origin Resource Sharing and that’s what I am going to explain in this post. To do this, I will use Angular.js and Sinatra. Let’s get started, shall we ?

Prerequisites

To follow this tutorial, you will need a version of Ruby and the Sinatra gem installed. You will also need a webserver, if you don’t have any check my post about SimpleHTTPServer.

Setup

So here is the basic code nothing fancy, a very very simple index.html, an angular module called MyApp completly empty and a Sinatra app with the basic route /hi coming from the documentation.

HTML

<!doctype html>
<html ng-app="myApp">
    <head>
        <title>CORS App</title>
    </head>
    <body>
        Cross-Origin Resource Sharing
        <script src="http://code.angularjs.org/1.1.5/angular.min.js"></script>
        <script src="app.js"></script>
    </body>
</html>

JS

var app = angular.module('myApp', []);

Ruby

require 'sinatra'
get '/hi' do
  "Hello World!"
end

Just create three files : index.html, app.js and server.rb and Copy/Paste the corresponding code in each file.

Run the code

You can run the Sinatra server with ruby server.rb and you can use any webserver to access index.html at http://localhost:9000. Indeed, CORS calls won’t work if you use the file url, you need a real domain.

Let’s get started by configuring the Sinatra app (don’t forget to restart Sinatra server) :

Ruby

require 'sinatra'
require 'json'

before do
   content_type :json      
end

set :protection, false

get '/movie' do
  { result: "Monster University" }.to_json
end

post '/movie' do
  { result: params[:movie] }.to_json
end

Okay, we are ready to go. If you access http://localhost:4567/movie from your browser, you should be able to see the last movie I saw. So let’s make our first javascript HTTP GET call.

CORS Get

HTML

<!doctype html>
<html ng-app="myApp">
    <head>
        <title>CORS App</title>
    </head>
    <body ng-controller="MainCtrl">
        Cross-Origin Resource Sharing<br/>

        <button ng-click="get()">GET</button>
        Result : {{result}}
        <script src="http://code.angularjs.org/1.1.5/angular.min.js"></script>
        <script src="app.js"></script>
    </body>
</html>

JS

var app = angular.module('myApp', []);

app.controller('MainCtrl', function($scope, $http) {

    $scope.get = function() {
        $http.get("http://localhost:4567/movie").success(function(result) {
            console.log("Success", result);
            $scope.result = result;
        }).error(function() {
            console.log("error");
        });
    };

});

Open it in your browser. You should see this beautiful red line saying that you cannot access this resouce due to the Same-origin policy.

XMLHttpRequest cannot load http://localhost:4567/movie. Origin http://localhost:8000 is not allowed by Access-Control-Allow-Origin.

Time to fix that with Cross-origin Resource Sharing !

JS

  var app = angular.module('myApp', []);

  app.config(function($httpProvider) {
      //Enable cross domain calls
      $httpProvider.defaults.useXDomain = true;

      //Remove the header used to identify ajax call  that would prevent CORS from working
      delete $httpProvider.defaults.headers.common['X-Requested-With'];
  });

  app.controller('MainCtrl', function($scope, $http) {

    $scope.get = function() {
      $http.get("http://localhost:4567/movie").success(function(result) {
          console.log("Success", result);
          $scope.result = result;
      }).error(function() {
          console.log("error");
      });
    };

  });

Ruby

require 'sinatra'
require 'json'

before do
   content_type :json    
   headers 'Access-Control-Allow-Origin' => '*', 
            'Access-Control-Allow-Methods' => ['OPTIONS', 'GET', 'POST']  
end

set :protection, false

options '/movie' do
    200
end

get '/movie' do
  { result: "Monster University" }.to_json
end

post '/movie' do
  { result: params[:movie] }.to_json
end

Restart Sinatra server and tadaaaa ! Now it’s working \o/ So what did we do ?

Well, first we told the $http module that we were going to send requests to another domain. We also removed the header used by the browser/server to identify our call as XmlHTTPRequest. Then, we enabled CORS on the server by specifying the available HTTP methods and the allowed origins (in our case, any origin *).

You probably noticed that we added a new route on our server :

options '/movie' do

This is part of the Cross-Origin Resource Sharing specification. Before sending a request to another domain, a call with the HTTP method OPTIONS will be fired. The response to this call will determine if CORS is available or not. This response must contain the allowed origins and the available HTTP methods

Security notice

In a production environment, you should not accept any origin of course, you should specify the allowed domain names like this :

headers 'Access-Control-Allow-Origin' => 'http://localhost:9000, http://localhost:8000'

You should also keep the default Sinatra protection enabled. However, you may have to disable the http origin security to make CORS calls work with Sinatra.

set :protection, except: :http_origin

Unfortunately, we are not done yet. Let’s add a post call and see it miserably fail… :

CORS Post

HTML

<!doctype html>
<html ng-app="myApp">
    <head>
        <title>CORS App</title>
    </head>
    <body ng-controller="MainCtrl">
        Cross-Origin Resource Sharing<br/>

        <button ng-click="get()">GET</button>
        Result : {{resultGet}}
        <br/>
        <input ng-model="movie"/><button ng-click="post(movie)">POST</button>
        Result : {{resultPost}}
        <script src="http://code.angularjs.org/1.1.5/angular.min.js"></script>
        <script src="app.js"></script>
    </body>
</html>

JS

var app = angular.module('myApp', []);

app.config(function($httpProvider) {
    //Enable cross domain calls
    $httpProvider.defaults.useXDomain = true;

    //Remove the header containing XMLHttpRequest used to identify ajax call 
    //that would prevent CORS from working
    delete $httpProvider.defaults.headers.common['X-Requested-With'];
});

app.controller('MainCtrl', function($scope, $http) {

    $scope.get = function() {
        $http.get("http://localhost:4567/movie").success(function(result) {
            console.log("Success", result);
            $scope.resultGet = result;
        }).error(function() {
            console.log("error");
        });
    };

    $scope.post = function(value) {
        $http.post("http://localhost:4567/movie", { 'movie': value }).success(function(result) {
            console.log(result);
            $scope.resultPost = result;
        }).error(function() {
            console.log("error");
        });
    };

});

Yay, we got a new error :

OPTIONS http://localhost:4567/movie Request header field Content-Type is not allowed by Access-Control-Allow-Headers. angular.min.js:106

XMLHttpRequest cannot load http://localhost:4567/movie. Request header field Content-Type is not allowed by Access-Control-Allow-Headers.

So what’s wrong ?

The answer is in the error, we need to add Content-Type to the allowed headers :

 headers 'Access-Control-Allow-Origin' => '*', 
        'Access-Control-Allow-Methods' => ['OPTIONS', 'GET', 'POST'],
        'Access-Control-Allow-Headers' => 'Content-Type'

Now, Post requests are working. But the server doest not send back the submitted word. Instead, we receive : Object {result: null}

If we add a trace to check the received parameters on the server, we get, well nothing :

I, [2013-08-14T21:11:04.901188 #28960]  INFO -- : Params : {}

That is due to how Angular.js handles the params to be sent with the post. Sinatra does not detect the params. We are going to do it by ourselves.

Ruby

require 'sinatra'
require 'json'

use Rack::Logger

before do
   content_type :json    
   headers 'Access-Control-Allow-Origin' => '*', 
           'Access-Control-Allow-Methods' => ['OPTIONS', 'GET', 'POST'],
           'Access-Control-Allow-Headers' => 'Content-Type'  
end

set :protection, false

options '/movie' do
    200
end

get '/movie' do
  { result: "Monster University" }.to_json
end

post '/movie' do

  begin
    params.merge! JSON.parse(request.env["rack.input"].read)
  rescue JSON::ParserError
    logger.error "Cannot parse request body." 
  end

  { result: params[:movie], seen: true }.to_json
end

With this, it should work fine.

There is an other way to get the params, but they will be poorly formatted. You can add the following to app.js :

$httpProvider.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";

With this, we tell the server that our params come from a form. This is how Sinatra handles it :

I, [2013-08-14T21:42:05.648475 #30008]  INFO -- : Params : {"{\"movie\":\"aaaa\",\"rating\":5,\"comment\":\"Super fun !\"}"=>nil}

As I told you, it’s kinda weird. Well, you can easily clean it up by getting the first key of the hash, parse it and get the params hash, that’s up to you.

Well, now you should be able to setup CORS calls between your client and your backend ! If you have any problem, feel free to contact me or leave a comment. It took me a while to figure out everything and I hope it will save you a lot of time. If you are interested in seeing PUT and DELETE calls, leave a comment and I will add it.

The code is available on Github.