Thursday, July 10, 2008

My mochiweb webservice...

Now that mochiweb is popular, and that I use it for some of my work, I can publish here some webservice thingy that I use.

I wanted to have this simple thing: modulename,funname, and parameters inside the URL:

http://www.server.tld/modulename/fun/arg1/arg2
wmodulename:Fun(arg1, arg2)

(Note the 'w' prefix, I explain it later)

Here's the code:

-module(webservice).
-export([start/0, start/2, loop/2, stop/0, test/0]).
-export([dolog/2]).
-define(PORT, 8080).

start() ->
start("/home/rolphin/Devel/Web", ?PORT).

start(Wwwroot, Port) ->
Loop = fun (Req) ->
?MODULE:loop(Req, Wwwroot)
end,
mochiweb_http:start([{loop, Loop}, {name, ?MODULE}, {port, Port}]).

stop() ->
mochiweb_http:stop(?MODULE).

loop(Req, DocRoot) ->
log(Req),
case string:tokens(Req:get(path), "/") of
[ "dump" ] ->
Req:ok({"text/plain",
io_lib:format("~p~n", [Req:dump()])});

[ "favicon.ico" ] ->
Req:respond({404, [], ""});

[ "codepath" ] ->
Req:ok({"text/plain",
io_lib:format("codepath: ~p~n", [code:get_path()])});

[ "codepath", "json" ] ->
Req:ok({"text/plain",
mochijson:encode({array, code:get_path()})});

[ Path, Fun | Elems ] ->
% Every module name should begin with 'w'
dispatch(Req, DocRoot, list_to_atom("w" ++ Path), Fun, Elems);

[] ->
launch(Req, DocRoot, wdefault, do, []);

_ ->
Req:respond({502, [], []})

end.

dispatch(Req, DocRoot, Module, Fun, Elems) ->
M = Req:get(method),
case M of
'GET' ->
launch(Req, DocRoot, Module, Fun, Elems);
'POST' ->
launch(Req, DocRoot, Module, Fun, Elems);
'PUT' ->
launch(Req, DocRoot, Module, Fun, Elems);
'DELETE' ->
launch(Req, DocRoot, Module, Fun, Elems);
'HEAD' ->
launch(Req, DocRoot, Module, Fun, Elems);
_Any ->
launch(Req, DocRoot, wdefault, get, [])
end.

launch(Req, DocRoot, wcontent, Fun, Args) ->
case catch wcontent:default(Req, DocRoot, [ Fun | Args] ) of
{'EXIT', {Type, _Error}} ->
Req:ok({"text/plain",
io_lib:format("GET Error: '~p' for '~p' ~p ~p~n~p~n", [Type, wcontent, Fun, Args, _Error])});
_ ->
ok
end;

launch(Req, DocRoot, Module, Fun, Args) ->
F = list_to_atom(Fun),
case catch Module:F(Req, DocRoot, Args) of
{'EXIT', {Type, _Error}} ->
Req:ok({"text/plain",
io_lib:format("~p Error: '~p' for ~p ~p ~p~n~p~n", [Req:get(method), Type, Module, Fun, Args, _Error])});
_ ->
ok
end.

log(Req) ->
Ip = Req:get(peer),
spawn(?MODULE, dolog, [Req, Ip]).

dolog(Req, Ip) ->
stat_logger:log("~p ~p", [Ip, Req:get(path)]).



First you can see that I use "mochiweb:start" and "mochiweb:stop" in the "start" and "stop" funs.
Then parameters are extracted from the URI:

case string:tokens(Req:get(path), "/") of

I split the URI on the "/" character, and compare the resulting list with various possibilities.

Then the main part of the code:

launch(Req, DocRoot, Module, Fun, Args) ->
F = list_to_atom(Fun),
case catch Module:F(Req, DocRoot, Args) of
{'EXIT', {Type, _Error}} ->
Req:ok({"text/plain",
io_lib:format("~p Error: '~p' for ~p ~p ~p~n~p~n", [Req:get(method), Type, Module, Fun, Args, _Error])});
_ ->
ok
end.

Using "catch" prevent your app from crashing without any information. If a module doesn't exists, you'll be able to see why (the main problem is sometimes the module is not in the code:path...)

Every webservice module name should start with "w", it's some sort of namespace :), now you know the reason for the 'w' prefix:

[ Path, Fun | Elems ] ->
% Every module name should begin with 'w'
dispatch(Req, DocRoot, list_to_atom("w" ++ Path), Fun, Elems);


Now, how can I use it ? It's simple, you just have to build a module with such a template:

-module(wsample).
-export([do/3]).

do(Req, _DocRoot, Args) ->
Req:ok({"text/plain", [ <<"Hello ">>, hd(Args) , <<", you are authorized :)">>] }).



Then you build it, and store it somewhere in your "code:path". And call

webservice:start().
in your erl shell. Then locate your webrowser to the URI
www.server.tld:8080/sample/do/PUT-ANYTHING-YOU-WANT-HERE


As a sample module, here is a module that is used everyday the "mobile tags" generator:

-module(wbarcode).
-export([do/3]).

-define(PNG, "image/png").

do(Req, _DocRoot, _Args) ->
barcode(Req).

% the process 'barcode' must be there
barcode(Req) ->
Options = Req:parse_qs(),
%io:format("~p~n", [Options]),

Text = proplists:get_value("text", Options, "http://www.shootit.fr"),
Res = proplists:get_value("res", Options, "72"),
Z = proplists:get_value("z", Options, "1"),

case barcode:generate('barcode', Text) of
timeout ->
Req:ok({"text/plain", [], <<"Timeout">>});

{data, {Width, Height}, Data} ->
Zoom = list_to_integer(Z),
Xres = integer_to_list(list_to_integer(Res) * Zoom),
Yres = integer_to_list(list_to_integer(Res) * Zoom),
Params = [ {"PS", [integer_to_list(Width + 1), integer_to_list(Height + 1)]}, {"HW", [Xres, Yres]} ],
case gs:draw(gs, Data, Params) of
{png, Image} ->
Req:ok({?PNG, Image});

_E ->
Req:ok({"text/plain", [], [ <<"Error: ">>,
io_lib:format("~s", [_E])]})
end
end.

9 comments:

Unknown said...

I started using mochiweb for rest style web services and it is looking very promising.

quick question - how do i do code swap with mochiweb - i was looking for a swap message but i could'nt locate it. What should i send to mochiweb to pickup an updated module
-Thanks
Bharani

Antoine said...

If you call the module using the its full name ie Module:fun, erlang will be pick the new one.

For more information you can lookat the mochiweb project on google-code, browse the source, and you'll see code dedicated to reload things up...

Andrew Dashin said...

Hi!

Why don't you use yaws/erlyweb for such purposes?

Antoine said...

Humm, I would say, for exactly the same reason I don't use APACHE but a lighter one...
Code simplicity, and nice little funs available from the mochiweb stuff...

The only problem with mochiweb, is that mochiweb sends response headers in one packet and the result in another... This is not a really good choice from a performance point of view...

From erlyweb, I would use erltl the template engine, which is great.

Papipo said...

@rolphin: Why does mochiweb send the responses in such manner? Doesn't it make sense to send them in the most performance-wise way?

Anyway, I guess that it should not be too hard to reimplement such thing and make it to send just one packet.

Antoine said...

The reason is for code simplicity, headers are fully handled by mochiweb, and what you send in the response body is up to you.

Implementing the efficient way by using a single packet would break the mochiweb simple code by now.

This is not impossible of course, and I may do it later.

Anonymous said...

>>Why does mochiweb send the responses in such manner? Doesn't it make sense to send them in the most performance-wise way?

One reason why is probably just that it is a non-issue for MochiWeb's original use case, which is to only serve requests proxied to it by Nginx. Nginx probably buffers the headers and waits for the response data, thus the headers would not get sent to the browser in a separate packet, it would all be sent as one by Nginx.

Papipo said...

@jon That makes sense :o)

Stefan Scholl said...

Your use of list_to_atom/1 could be dangerous: http://www.erlang.org/doc/efficiency_guide/commoncaveats.html#3.3

Sticky