-
Notifications
You must be signed in to change notification settings - Fork 38
/
elli_example_callback.erl
329 lines (275 loc) · 12.4 KB
/
elli_example_callback.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
%%% @doc: Elli example callback
%%%
%%% Your callback needs to implement two functions, {@link handle/2} and
%%% {@link handle_event/3}. For every request, Elli will call your handle
%%% function with the request. When an event happens, like Elli
%%% completed a request, there was a parsing error or your handler
%%% threw an error, {@link handle_event/3} is called.
-module(elli_example_callback).
-export([handle/2, handle_event/3]).
-export([chunk_loop/1]).
-include("elli.hrl").
-include("elli_util.hrl").
-behaviour(elli_handler).
-include_lib("kernel/include/file.hrl").
%%
%% ELLI REQUEST CALLBACK
%%
%% @doc Handle a `Req'uest.
%% Delegate to our handler function.
%% @see handle/3
-spec handle(Req, _Args) -> Result when
Req :: elli:req(),
_Args :: elli_handler:callback_args(),
Result :: elli_handler:result().
handle(Req, _Args) -> handle(Req#req.method, elli_request:path(Req), Req).
%% @doc Route `Method' and `Path' to the appropriate clause.
%%
%% `ok' can be used instead of `200' to signal success.
%%
%% If you return any of the following HTTP headers, you can
%% override the default behaviour of Elli:
%%
%% * **Connection**: By default Elli will use `keep-alive' if the protocol
%% supports it, setting `<<"close">>' will close the
%% connection immediately after Elli has sent the
%% response. If the client has already sent pipelined
%% requests, these will be discarded.
%%
%% * **Content-Length**: By default Elli looks at the size of the body you
%% returned to determine the `Content-Length' header.
%% Explicitly including your own `Content-Length' (with
%% the value as `integer()', `binary()' or `list()')
%% allows you to return an empty body. Useful for
%% implementing the `"304 Not Modified"' response.
%%
%% @see elli_request:get_arg/3
%% @see elli_request:post_arg/3
%% @see elli_request:post_arg_decoded/3
%% @see elli_request:get_header/3
%% @see elli_request:get_arg_decoded/3
%% @see elli_request:get_args_decoded/1
%% @see elli_util:file_size/1
%% @see elli_request:get_range/1
%% @see elli_request:normalize_range/2
%% @see elli_request:encode_range/2
%% @see elli_request:chunk_ref/1
%% @see chunk_loop/1
-spec handle(Method, Path, Req) -> elli_handler:result() when
Method :: elli:http_method(),
Path :: [binary()],
Req :: elli:req().
handle('GET', [<<"hello">>, <<"world">>], _Req) ->
%% Reply with a normal response.
timer:sleep(1000),
{ok, [], <<"Hello World!">>};
handle('GET', [<<"hello">>], Req) ->
%% Fetch a GET argument from the URL.
Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>),
{ok, [], <<"Hello ", Name/binary>>};
handle('POST', [<<"hello">>], Req) ->
%% Fetch a POST argument from the POST body.
Name = elli_request:post_arg(<<"name">>, Req, <<"undefined">>),
%% Fetch and decode
City = elli_request:post_arg_decoded(<<"city">>, Req, <<"undefined">>),
{ok, [], <<"Hello ", Name/binary, " of ", City/binary>>};
handle('GET', [<<"hello">>, <<"iolist">>], Req) ->
%% Iolists will be kept as iolists all the way to the socket.
Name = elli_request:get_arg(<<"name">>, Req),
{ok, [], [<<"Hello ">>, Name]};
handle('GET', [<<"type">>], Req) ->
Name = elli_request:get_arg(<<"name">>, Req),
%% Fetch a header.
case elli_request:get_header(<<"Accept">>, Req, <<"text/plain">>) of
<<"text/plain">> ->
{ok, [{<<"content-type">>, <<"text/plain; charset=ISO-8859-1">>}],
<<"name: ", Name/binary>>};
<<"application/json">> ->
{ok, [{<<"content-type">>,
<<"application/json; charset=ISO-8859-1">>}],
<<"{\"name\" : \"", Name/binary, "\"}">>}
end;
handle('GET', [<<"headers.html">>], _Req) ->
%% Set custom headers, for example 'Content-Type'
{ok, [{<<"X-Custom">>, <<"foobar">>}], <<"see headers">>};
%% See note in function doc re: overriding Elli's default behaviour
%% via Connection and Content-Length headers.
handle('GET', [<<"user">>, <<"defined">>, <<"behaviour">>], _Req) ->
{304, [{<<"Connection">>, <<"close">>},
{<<"Content-Length">>, <<"123">>}], <<"ignored">>};
handle('GET', [<<"user">>, <<"content-length">>], _Req) ->
{200, [{<<"Content-Length">>, 123}], <<"foobar">>};
handle('GET', [<<"crash">>], _Req) ->
%% Throwing an exception results in a 500 response and
%% request_throw being called
throw(foobar);
handle('GET', [<<"decoded-hello">>], Req) ->
%% Fetch a URI decoded GET argument from the URL.
Name = elli_request:get_arg_decoded(<<"name">>, Req, <<"undefined">>),
{ok, [], <<"Hello ", Name/binary>>};
handle('GET', [<<"decoded-list">>], Req) ->
%% Fetch a URI decoded GET argument from the URL.
[{<<"name">>, Name}, {<<"foo">>, true}] =
elli_request:get_args_decoded(Req),
{ok, [], <<"Hello ", Name/binary>>};
handle('GET', [<<"sendfile">>], _Req) ->
%% Returning {file, "/path/to/file"} instead of the body results
%% in Elli using sendfile.
F = filename:join(code:priv_dir(elli), "README.md"),
{ok, [], {file, F}};
handle('GET', [<<"send_no_file">>], _Req) ->
%% Returning {file, "/path/to/file"} instead of the body results
%% in Elli using sendfile.
F = "README",
{ok, [], {file, F}};
handle('GET', [<<"sendfile">>, <<"error">>], _Req) ->
F = code:priv_dir(elli),
{ok, [], {file, F}};
handle('GET', [<<"sendfile">>, <<"range">>], Req) ->
%% Read the Range header of the request and use the normalized
%% range with sendfile, otherwise send the entire file when
%% no range is present, or respond with a 416 if the range is invalid.
F = filename:join(code:priv_dir(elli), "README.md"),
{ok, [], {file, F, elli_request:get_range(Req)}};
handle('GET', [<<"compressed">>], _Req) ->
%% Body with a byte size over 1024 are automatically gzipped by
%% elli_middleware_compress
{ok, binary:copy(<<"Hello World!">>, 86)};
handle('GET', [<<"compressed-io_list">>], _Req) ->
%% Body with a iolist size over 1024 are automatically gzipped by
%% elli_middleware_compress
{ok, lists:duplicate(86, [<<"Hello World!">>])};
handle('HEAD', [<<"head">>], _Req) ->
{200, [], <<"body must be ignored">>};
handle('GET', [<<"chunked">>], Req) ->
%% Start a chunked response for streaming real-time events to the
%% browser.
%%
%% Calling elli_request:send_chunk(ChunkRef, Body) will send that
%% part to the client. elli_request:close_chunk(ChunkRef) will
%% close the response.
%%
%% Return immediately {chunk, Headers} to signal we want to chunk.
Ref = elli_request:chunk_ref(Req),
spawn(fun() -> ?MODULE:chunk_loop(Ref) end),
{chunk, [{<<"Content-Type">>, <<"text/event-stream">>}]};
handle('GET', [<<"shorthand">>], _Req) ->
{200, <<"hello">>};
handle('GET', [<<"ip">>], Req) ->
{<<"200 OK">>, elli_request:peer(Req)};
handle('GET', [<<"304">>], _Req) ->
%% A "Not Modified" response is exactly like a normal response (so
%% Content-Length is included), but the body will not be sent.
{304, [{<<"Etag">>, <<"foobar">>}], <<"Ignored">>};
handle('GET', [<<"302">>], _Req) ->
{302, [{<<"Location">>, <<"/hello/world">>}], <<>>};
handle('GET', [<<"403">>], _Req) ->
%% Exceptions formatted as return codes can be used to
%% short-circuit a response, for example in case of
%% authentication/authorization
throw({403, [], <<"Forbidden">>});
handle('GET', [<<"invalid_return">>], _Req) ->
{invalid_return};
handle(_, _, _Req) ->
{404, [], <<"Not Found">>}.
%% @doc Send 10 separate chunks to the client.
%% @equiv chunk_loop(Ref, 10)
chunk_loop(Ref) ->
chunk_loop(Ref, 10).
%% @doc If `N > 0', send a chunk to the client, checking for errors,
%% as the user might have disconnected.
%% When `N == 0', call {@link elli_request:close_chunk/1.
%% elli_request:close_chunk(Ref)}.
chunk_loop(Ref, 0) ->
elli_request:close_chunk(Ref);
chunk_loop(Ref, N) ->
timer:sleep(10),
case elli_request:send_chunk(Ref, [<<"chunk">>, integer_to_binary(N)]) of
ok -> ok;
{error, Reason} -> ?LOG_ERROR("error in sending chunk: ~p~n", [Reason])
end,
chunk_loop(Ref, N-1).
%%
%% ELLI EVENT CALLBACKS
%%
%% @doc Handle Elli events, fired throughout processing a request.
%%
%% `elli_startup' is sent when Elli is starting up. If you are
%% implementing a middleware, you can use it to spawn processes,
%% create ETS tables or start supervised processes in a supervisor
%% tree.
%%
%% `request_complete' fires *after* Elli has sent the response to the
%% client. `Timings' contains timestamps (native units) of events like when the
%% connection was accepted, when headers/body parsing finished, when the
%% user callback returns, response sent, etc. `Sizes' contains response sizes
%% like response headers size, response body or file size.
%% This allows you to collect performance statistics for monitoring your app.
%%
%% `request_throw', `request_error' and `request_exit' events are sent if
%% the user callback code throws an exception, has an error or
%% exits. After triggering this event, a generated response is sent to
%% the user.
%%
%% `invalid_return' is sent if the user callback code returns a term not
%% understood by elli, see {@link elli_http:execute_callback/1}.
%% After triggering this event, a generated response is sent to the user.
%%
%% `chunk_complete' fires when a chunked response is completely
%% sent. It's identical to the `request_complete' event, except instead
%% of the response body you get the atom `client' or `server'
%% depending on who closed the connection. `Sizes' will have the key `chunks',
%% which is the total size of all chunks plus encoding overhead.
%%
%% `request_closed' is sent if the client closes the connection when
%% Elli is waiting for the next request on a keep alive connection.
%%
%% `request_timeout' is sent if the client times out when
%% Elli is waiting for the request.
%%
%% `request_parse_error' fires if the request is invalid and cannot be parsed by
%% [`erlang:decode_packet/3`][decode_packet/3] or it contains a path Elli cannot
%% parse or does not support.
%%
%% [decode_packet/3]: http://erlang.org/doc/man/erlang.html#decode_packet-3
%%
%% `client_closed' can be sent from multiple parts of the request
%% handling. It's sent when the client closes the connection or if for
%% any reason the socket is closed unexpectedly. The `Where' atom
%% tells you in which part of the request processing the closed socket
%% was detected: `receiving_headers', `receiving_body' or `before_response'.
%%
%% `client_timeout' can as with `client_closed' be sent from multiple
%% parts of the request handling. If Elli tries to receive data from
%% the client socket and does not receive anything within a timeout,
%% this event fires and the socket is closed.
%%
%% `bad_request' is sent when Elli detects a request is not well
%% formatted or does not conform to the configured limits. Currently
%% the `Reason' variable can be `{too_many_headers, Headers}'
%% or `{body_size, ContentLength}'.
%%
%% `file_error' is sent when the user wants to return a file as a
%% response, but for some reason it cannot be opened.
-spec handle_event(Event, Args, Config) -> ok when
Event :: elli_handler:event(),
Args :: elli_handler:callback_args(),
Config :: [tuple()].
handle_event(elli_startup, [], _) -> ok;
handle_event(request_complete, [_Request,
_ResponseCode, _ResponseHeaders, _ResponseBody,
{_Timings, _Sizes}], _) -> ok;
handle_event(request_throw, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(request_error, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(request_exit, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(invalid_return, [_Request, _ReturnValue], _) -> ok;
handle_event(chunk_complete, [_Request,
_ResponseCode, _ResponseHeaders, _ClosingEnd,
{_Timings, _Sizes}], _) -> ok;
handle_event(request_closed, [], _) -> ok;
handle_event(request_timeout, [], _) -> ok;
handle_event(request_parse_error, [_], _) -> ok;
handle_event(client_closed, [_Where], _) -> ok;
handle_event(client_timeout, [_Where], _) -> ok;
handle_event(bad_request, [_Reason], _) -> ok;
handle_event(file_error, [_ErrorReason], _) -> ok.