備忘録、はじめました。

作業したこと忘れないようにメモっておきます。

webmachineとAJAX

はじめに

 2014年5月30日現在、アプリケーションの開発で、バックエンドにwebmachine、フロントエンドにAngularJSを選択しようと考えています。その2つのやりとりでCORSの問題が起きるので、回避方法をメモします。主にやることは、webmachine側のAccess-Control-Allow-OriginのHeaderの追加と、許可するContent-typeなどの設定になります。

webmachineのResourse Functions API

webmachineのresouceに対する処理は基本的にRESTになるように設計がされています。あるresourceに対して許可するメソッドを設定し、各メソッドの処理を記述していきます。resourceの処理を記述した一例を以下に載せます。

.src/appname_ebooks_resource.erl

-module(appname_ebooks_resource).
-export([
         init/1,
         allowed_methods/2,
         content_types_accepted/2,
         content_types_provided/2,
         post_is_create/2,
         create_path/2,
         to_json/2,
         create_resource/2,
         options/2,
         delete_resource/2
        ]).

-include_lib("webmachine/include/webmachine.hrl").
-include_lib("eunit/include/eunit.hrl").

init([]) -> 
  {ok, undefined}.

%%%==================================================
%%% Setting of OPTIONS method (add headers to ReqData)
%%%==================================================

options(Req, State) ->
  {[
    {"Access-Control-Allow-Origin","*"},
    {"Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"}
   ], Req, State}.


%%%==================================================
%%% Allow Methods and Accepted Content Types
%%%==================================================

%% Allowed HTTP methods
allowed_methods(Req, State) ->
  {['GET','POST','PUT','DELETE','OPTIONS'], Req, State}.

%% Accepted content type
content_types_accepted(Req, State) ->
  {[
    {"application/json", create_resource}
   ], Req, State}.

%% Privoided content type
content_types_provided(Req,State) ->
  {[
    {"html/text", to_json},
    {"application/json", create_resource}
   ], Req, State}.

%%%==================================================
%%% Post settings
%%%==================================================

%% creating a new resource from post request
post_is_create(Req, State) ->
  {true, Req, State}.

%% this function is called when post_is_create/2 returns true.
%% return new resouce path( embeded 'Location' header).
create_path(Req,State) ->
  {"apis/ebooks", Req, State}.

%%%==================================================
%%% Handler functions
%%%==================================================

%% GET
to_json(#wm_reqdata{method = 'GET'} = Req, State) ->
  % TODO: Get process
  io:format("Request Data: ~n ~p~n",[wrq:req_qs(Req)]), 
  {<<"{\"json\":\"get test.\"}">>, Req, State}.

%% POST
create_resource(#wm_reqdata{method = 'POST'} = Req, State) ->
  % TODO: create process
  Req2 = wrq:set_resp_headers([
                               {"Access-Control-Allow-Origin", "*"}
                              ], Req),
  case mochijson2:decode(wrq:req_body(Req2)) of 
    {struct, Params} ->
      io:format("json put request(method:~p).~n", [Params]),
      EbookTitle = proplists:get_value(<<"title">>, Params),
      EbookNum = proplists:get_value(<<"number">>, Params),
      EbookDesc = proplists:get_value(<<"desc">>, Params),
      EbookPath = proplists:get_value(<<"path">>, Params),
      io:format("PostData: ~n title:~p~n number:~p~n desc:~p~n, book_path:~p~n",
                [EbookTitle,EbookNum,EbookDesc,EbookPath]),
      {true, Req2, State};
    _Other ->
      {false, Req2, State}
  end;
%% PUT
create_resource(#wm_reqdata{method = 'PUT'} = Req, State) ->
  % TODO: Update process
  io:format("json put request(method:~p).~n", [Req#wm_reqdata.method]),
  {true, Req, State}.


%% DELETE
delete_resource(Req, State) ->
  % TODO: Delete process
  io:format("delete request(method:~p).~n", [Req#wm_reqdata.method]),
  {true, Req, State}.

ちなみに、上記の内容はErlang製Webツールキットwebmachine 触ってみた - ごろねこ日記を参考にしました。
必要な箇所だけ、順に説明します。

options(Req, State)

optionsは、OPTIONSメソッドのリクエストに対して、追加設定を記述できるようになります。この機能はwebmachineが提供してくれるResource FunctionsAPIの一つです(詳しくはResource Functions · basho/webmachine Wiki · GitHub)。

%%%==================================================
%%% Setting of OPTIONS method (add headers to ReqData)
%%%==================================================

options(Req, State) ->
  {[
    {"Access-Control-Allow-Origin","*"},
    {"Access-Control-Allow-Headers", "Content-Type, Accept"}
   ], Req, State}.

内容は、Ajax等でXMLHttpRequestを送られたときに、Access-Control-Allow-OriginのHeaderを追加しクロスドメインの制約を緩めます。また、postする際にContent-typeを指定する場合、OPTIONSメソッドに許可するHeaderを明記する必要がある。書き方は、Access-Control-Allow-Headersとして記述する。この2つのHeaderを明記しないとアクセスができません。前者はクロスドメイン制約によるアクセス拒否、後者は受け入れ許可していないHeaderの混入によるアクセス拒否です。

create_resource(Req, State)

AJAX通信で受け取った内容を処理していきます。ここで注意することは、結果を返すときにもresponseとなる内容にクロスドメイン制約に関するHeaderを設定する必要があることです。以下に、ソースを載せます。

%% POST
create_resource(#wm_reqdata{method = 'POST'} = Req, State) ->
  % TODO: create process
  Req2 = wrq:set_resp_headers([
                               {"Access-Control-Allow-Origin", "*"}
                              ], Req),
  case mochijson2:decode(wrq:req_body(Req2)) of 
    {struct, Params} ->
      io:format("json put request(method:~p).~n", [Params]),
      EbookTitle = proplists:get_value(<<"title">>, Params),
      EbookNum = proplists:get_value(<<"number">>, Params),
      EbookDesc = proplists:get_value(<<"desc">>, Params),
      EbookPath = proplists:get_value(<<"path">>, Params),
      io:format("PostData: ~n title:~p~n number:~p~n desc:~p~n, book_path:~p~n",
                [EbookTitle,EbookNum,EbookDesc,EbookPath]),
      {true, Req2, State};
    _Other ->
      {false, Req2, State}
  end;

4行目の、wrq:set_resp_headers/2Access-Control-Allow-OriginのHeaderを追加します。wrqは、webmachineのRequest Data APIと呼ばれるもので、詳細はRequest Data API · basho/webmachine Wiki · GitHubに載っています。
Access-Control-Allow-Originを追加したReqを変数Req2に束縛し、return値のReqDataに渡します({true, Req2, State}の部分)。

あとは、jQueryなりで$.ajaxを使ってテストしてみます。そこは割愛します。

まとめと今後の課題

今回は、webmachineとajax通信する場合に起きるクロスドメイン制約の問題がありました。そして、このクロスドメイン制約を解消するための手順について説明しました。具体的なやり方は、1)OPTIONSメソッドで通信された時にレスポンス(既存のRequestデータ)にAccess-Control-Allow-OriginのHeaderを追加、2)メソッド処理の内部でも同様にAccess-Control-Allow-OriginのHeaderを追加、3)通信する際に特別Headerを追加する際にはOPTIONSメソッド通信の設定でAccess-Control-Allow-Headersに明記、の3点です。

今後は、resource部分の処理を詳細に書いて、テストを書き終わった後に、フロントエンドとの連携をしていきます。

余談

OPTIONSメソッドに対して新しくHeaderを追加していく方法が全然わからなかったところ、erlang - Enabling CORS for Cowboy REST API - Stack Overflowを見つけて、紐解きました。