-
-
Notifications
You must be signed in to change notification settings - Fork 456
Expand file tree
/
Copy pathtest_http_resource.erl
More file actions
238 lines (214 loc) · 9.23 KB
/
Copy pathtest_http_resource.erl
File metadata and controls
238 lines (214 loc) · 9.23 KB
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
%% @doc Generic test HTTP resource for hackney tests.
%% Replaces dependency on external httpbin server.
%%
%% Supported routes:
%% GET /get - return request info as JSON
%% POST /post - echo request info with body as JSON
%% QUERY /query - echo request info with body as JSON
%% HEAD /* - return headers only
%% GET /status/:code - return specific status code
%% GET /redirect-to - redirect to ?url= (optional ?status_code=)
%% GET /basic-auth/:u/:p - require basic auth
%% GET /cookies/set - set cookies from query params
%% GET /cookies - return cookies as JSON
%% GET /robots.txt - return fixed robots.txt content
%% GET /inform - send 1xx informational response before final response
-module(test_http_resource).
-export([init/2]).
init(Req, State) ->
Method = cowboy_req:method(Req),
Path = cowboy_req:path(Req),
handle_request(Method, Path, Req, State).
%% GET /get - return request info as JSON
handle_request(<<"GET">>, <<"/get">>, Req, State) ->
reply_json(200, request_info(Req), Req, State);
%% POST /post - echo request info with body
handle_request(<<"POST">>, <<"/post">>, Req0, State) ->
{ok, Body, Req} = cowboy_req:read_body(Req0),
Info = request_info(Req),
Info2 = Info#{<<"data">> => Body},
reply_json(200, Info2, Req, State);
%% QUERY /query - echo request info with body (RFC 10008: safe method with body)
handle_request(<<"QUERY">>, <<"/query">>, Req0, State) ->
{ok, Body, Req} = cowboy_req:read_body(Req0),
Info = request_info(Req),
Info2 = Info#{<<"data">> => Body},
reply_json(200, Info2, Req, State);
%% HEAD requests - return headers only (no body)
handle_request(<<"HEAD">>, _Path, Req, State) ->
Req2 = cowboy_req:reply(200, #{
<<"content-type">> => <<"application/json">>
}, Req),
{ok, Req2, State};
%% GET /status/:code - return specific status code
handle_request(<<"GET">>, <<"/status/", CodeBin/binary>>, Req, State) ->
Code = binary_to_integer(CodeBin),
%% For 204 and 304, no body should be sent
case Code of
204 ->
Req2 = cowboy_req:reply(204, #{}, Req),
{ok, Req2, State};
304 ->
Req2 = cowboy_req:reply(304, #{}, Req),
{ok, Req2, State};
_ ->
reply_json(Code, #{<<"status">> => Code}, Req, State)
end;
%% <method> /redirect-to?url=X&status_code=Y - redirect to URL (any method, so
%% 307/308 body-forwarding can be exercised for POST/PUT/PATCH too)
handle_request(_Method, <<"/redirect-to">>, Req, State) ->
QS = cowboy_req:parse_qs(Req),
Url = proplists:get_value(<<"url">>, QS, <<"/">>),
StatusCode = case proplists:get_value(<<"status_code">>, QS) of
undefined -> 302;
SC -> binary_to_integer(SC)
end,
Req2 = cowboy_req:reply(StatusCode, #{
<<"location">> => Url
}, Req),
{ok, Req2, State};
%% GET /basic-auth/:user/:pass - require basic auth
handle_request(<<"GET">>, <<"/basic-auth/", Rest/binary>>, Req, State) ->
[ExpectedUser, ExpectedPass] = binary:split(Rest, <<"/">>),
case cowboy_req:parse_header(<<"authorization">>, Req) of
{basic, User, Pass} when User =:= ExpectedUser, Pass =:= ExpectedPass ->
reply_json(200, #{
<<"authenticated">> => true,
<<"user">> => User
}, Req, State);
_ ->
Req2 = cowboy_req:reply(401, #{
<<"www-authenticate">> => <<"Basic realm=\"Fake Realm\"">>
}, <<"{\"authenticated\": false}">>, Req),
{ok, Req2, State}
end;
%% GET /cookies/set?k=v - set cookies
handle_request(<<"GET">>, <<"/cookies/set">>, Req, State) ->
QS = cowboy_req:parse_qs(Req),
%% Set each query param as a cookie
Req2 = lists:foldl(fun({Name, Value}, ReqAcc) ->
cowboy_req:set_resp_cookie(Name, Value, ReqAcc, #{path => <<"/">>})
end, Req, QS),
Req3 = cowboy_req:reply(200, #{
<<"content-type">> => <<"application/json">>
}, <<"{\"cookies\": \"set\"}">>, Req2),
{ok, Req3, State};
%% GET /cookies - return cookies as JSON
handle_request(<<"GET">>, <<"/cookies">>, Req, State) ->
Cookies = cowboy_req:parse_cookies(Req),
CookieMap = maps:from_list(Cookies),
reply_json(200, #{<<"cookies">> => CookieMap}, Req, State);
%% GET /robots.txt - fixed content
handle_request(<<"GET">>, <<"/robots.txt">>, Req, State) ->
Body = <<"User-agent: *\nDisallow: /deny\n">>,
Req2 = cowboy_req:reply(200, #{
<<"content-type">> => <<"text/plain">>
}, Body, Req),
{ok, Req2, State};
%% GET /chunked/:size - return chunked response with specified body size
%% For testing issue #403 - file download hangs
handle_request(<<"GET">>, <<"/chunked/", SizeBin/binary>>, Req, State) ->
Size = binary_to_integer(SizeBin),
%% Use stream_reply which uses chunked encoding in HTTP/1.1
Headers = #{<<"content-type">> => <<"application/octet-stream">>},
Req2 = cowboy_req:stream_reply(200, Headers, Req),
%% Send body in chunks
Body = binary:copy(<<"X">>, Size),
ok = cowboy_req:stream_body(Body, fin, Req2),
{ok, Req2, State};
%% GET /connection-close - return with Connection: close header
handle_request(<<"GET">>, <<"/connection-close">>, Req, State) ->
Req2 = cowboy_req:reply(200, #{
<<"content-type">> => <<"application/json">>,
<<"connection">> => <<"close">>
}, <<"{\"connection\": \"close\"}">>, Req),
{ok, Req2, State};
%% GET /maybe-close - randomly close the connection (~50%), to stress the pool
%% with a mix of keep-alive and Connection: close responses on the same URI.
handle_request(<<"GET">>, <<"/maybe-close">>, Req, State) ->
Headers = case rand:uniform(2) of
1 -> #{<<"content-type">> => <<"application/json">>,
<<"connection">> => <<"close">>};
2 -> #{<<"content-type">> => <<"application/json">>}
end,
Req2 = cowboy_req:reply(200, Headers, <<"{\"maybe\": \"close\"}">>, Req),
{ok, Req2, State};
%% GET /connection-close/:size - return large body with Connection: close
%% For testing issue #439 - responses with connection close sometimes lost
handle_request(<<"GET">>, <<"/connection-close/", SizeBin/binary>>, Req, State) ->
Size = binary_to_integer(SizeBin),
Body = binary:copy(<<"X">>, Size),
Req2 = cowboy_req:reply(200, #{
<<"content-type">> => <<"application/octet-stream">>,
<<"connection">> => <<"close">>
}, Body, Req),
{ok, Req2, State};
%% GET /inform - send 1xx informational response before final response
%% Query params:
%% - status: informational status code (default 103)
%% - link: value for Link header in informational response
handle_request(<<"GET">>, <<"/inform">>, Req0, State) ->
QS = cowboy_req:parse_qs(Req0),
InformStatus = case proplists:get_value(<<"status">>, QS) of
undefined -> 103;
StatusBin -> binary_to_integer(StatusBin)
end,
InformHeaders = case proplists:get_value(<<"link">>, QS) of
undefined -> #{};
LinkValue -> #{<<"link">> => LinkValue}
end,
%% Send informational response first
ok = cowboy_req:inform(InformStatus, InformHeaders, Req0),
%% Then send final response
reply_json(200, #{<<"informed">> => true, <<"inform_status">> => InformStatus}, Req0, State);
%% GET /compressed/gzip - return gzip compressed response
handle_request(<<"GET">>, <<"/compressed/gzip">>, Req, State) ->
Data = <<"Hello, this is gzip compressed data!">>,
CompressedData = zlib:gzip(Data),
Req2 = cowboy_req:reply(200, #{
<<"content-type">> => <<"text/plain">>,
<<"content-encoding">> => <<"gzip">>
}, CompressedData, Req),
{ok, Req2, State};
%% GET /compressed/deflate - return deflate compressed response
handle_request(<<"GET">>, <<"/compressed/deflate">>, Req, State) ->
Data = <<"Hello, this is deflate compressed data!">>,
Z = zlib:open(),
ok = zlib:deflateInit(Z),
CompressedData = iolist_to_binary(zlib:deflate(Z, Data, finish)),
ok = zlib:deflateEnd(Z),
zlib:close(Z),
Req2 = cowboy_req:reply(200, #{
<<"content-type">> => <<"text/plain">>,
<<"content-encoding">> => <<"deflate">>
}, CompressedData, Req),
{ok, Req2, State};
%% GET /compressed/none - return uncompressed response
handle_request(<<"GET">>, <<"/compressed/none">>, Req, State) ->
Data = <<"Hello, this is uncompressed data!">>,
Req2 = cowboy_req:reply(200, #{
<<"content-type">> => <<"text/plain">>
}, Data, Req),
{ok, Req2, State};
%% Fallback - return 404
handle_request(_Method, _Path, Req, State) ->
Req2 = cowboy_req:reply(404, #{
<<"content-type">> => <<"application/json">>
}, <<"{\"error\": \"not found\"}">>, Req),
{ok, Req2, State}.
%% Helper: build request info as a map
request_info(Req) ->
Headers = cowboy_req:headers(Req),
#{
<<"headers">> => Headers,
<<"method">> => cowboy_req:method(Req),
<<"path">> => cowboy_req:path(Req),
<<"url">> => cowboy_req:uri(Req)
}.
%% Helper: reply with JSON (accepts map)
reply_json(Status, Data, Req, State) when is_map(Data) ->
Body = jsx:encode(Data),
Req2 = cowboy_req:reply(Status, #{
<<"content-type">> => <<"application/json">>
}, Body, Req),
{ok, Req2, State}.