Skip to content

Commit 41c1372

Browse files
committed
Replace cowboy test server with gen_tcp
cowboy 2.13+ publishes compound hex version constraints (">= 2.16.0 and < 3.0.0") that the rebar3 bundled with the erlang:25 CI image cannot parse, and no rebar3 release that can parse them still runs on OTP 25. buoy only used cowboy as the eunit HTTP fixture, so replace it with a ~130-line gen_tcp server speaking just enough HTTP/1.1 (keep-alive, content-length bodies, chunked transfer, HEAD) to serve the five test routes, and drop the test profile entirely.
1 parent 04ed140 commit 41c1372

2 files changed

Lines changed: 113 additions & 43 deletions

File tree

rebar.config

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@
3535
warn_unused_import,
3636
warn_unused_vars
3737
]}
38-
]},
39-
{test, [
40-
{deps, [
41-
{cowboy, "2.15.0"}
42-
]}
4338
]}
4439
]}.
4540

test/buoy_http_server.erl

Lines changed: 113 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,124 @@
55
stop/0
66
]).
77

8-
-export([
9-
init/2
10-
]).
8+
-define(PORT, 8080).
119

1210
%% public
1311
start() ->
14-
application:ensure_all_started(cowboy),
15-
Dispatch = cowboy_router:compile([{'_', [
16-
{"/:test", ?MODULE, []}]}]),
17-
{ok, _} = cowboy:start_clear(?MODULE, [{port, 8080}], #{
18-
env => #{dispatch => Dispatch},
19-
max_keepalive => infinity,
20-
request_timeout => infinity
21-
}).
12+
Self = self(),
13+
Pid = spawn(fun () -> init(Self) end),
14+
receive
15+
{Pid, started} ->
16+
{ok, Pid}
17+
after 5000 ->
18+
{error, timeout}
19+
end.
2220

2321
stop() ->
24-
cowboy:stop_listener(?MODULE).
25-
26-
%% cowboy callbacks
27-
init(Req, State) ->
28-
case cowboy_req:binding(test, Req) of
29-
<<"1">> ->
30-
reply(200, <<"Hello world!">>, Req, State);
31-
<<"2">> ->
32-
Body = [<<"Hello world!">> || _ <- lists:seq(1, 1000)],
33-
reply(200, Body, Req, State);
34-
<<"3">> ->
35-
{ok, Body, Req2} = cowboy_req:read_body(Req),
36-
reply(200, Body, Req2, State);
37-
<<"4">> ->
38-
Req2 = cowboy_req:stream_reply(200, Req),
39-
ok = cowboy_req:stream_body("Hello", nofin, Req2),
40-
ok = cowboy_req:stream_body(" world!", fin, Req2),
41-
{ok, Req2, State};
42-
<<"5">> ->
43-
Verb = cowboy_req:method(Req),
44-
reply(200, Verb, Req, State)
22+
case whereis(?MODULE) of
23+
undefined ->
24+
ok;
25+
Pid ->
26+
exit(Pid, kill),
27+
ok
4528
end.
4629

4730
%% private
48-
reply(StatusCode, Body, Req, State) ->
49-
Req2 = cowboy_req:reply(StatusCode, #{
50-
<<"Content-Type">> => <<"text/plain">>,
51-
<<"Connection">> => <<"Keep-Alive">>
52-
}, Body, Req),
53-
{ok, Req2, State}.
31+
init(Parent) ->
32+
register(?MODULE, self()),
33+
{ok, LSocket} = gen_tcp:listen(?PORT, [
34+
binary,
35+
{active, false},
36+
{backlog, 128},
37+
{packet, http_bin},
38+
{reuseaddr, true}
39+
]),
40+
Parent ! {self(), started},
41+
accept(LSocket).
42+
43+
accept(LSocket) ->
44+
{ok, Socket} = gen_tcp:accept(LSocket),
45+
Pid = spawn_link(fun () ->
46+
receive go -> connection(Socket) end
47+
end),
48+
ok = gen_tcp:controlling_process(Socket, Pid),
49+
Pid ! go,
50+
accept(LSocket).
51+
52+
connection(Socket) ->
53+
case gen_tcp:recv(Socket, 0) of
54+
{ok, {http_request, Method, {abs_path, Path}, _Version}} ->
55+
ContentLength = headers(Socket, 0),
56+
Body = body(Socket, ContentLength),
57+
respond(Socket, Method, Path, Body),
58+
connection(Socket);
59+
{ok, _} ->
60+
gen_tcp:close(Socket);
61+
{error, _} ->
62+
gen_tcp:close(Socket)
63+
end.
64+
65+
headers(Socket, ContentLength) ->
66+
case gen_tcp:recv(Socket, 0) of
67+
{ok, {http_header, _, 'Content-Length', _, Value}} ->
68+
headers(Socket, binary_to_integer(Value));
69+
{ok, {http_header, _, _, _, _}} ->
70+
headers(Socket, ContentLength);
71+
{ok, http_eoh} ->
72+
ContentLength
73+
end.
74+
75+
body(_Socket, 0) ->
76+
<<>>;
77+
body(Socket, ContentLength) ->
78+
ok = inet:setopts(Socket, [{packet, raw}]),
79+
{ok, Body} = gen_tcp:recv(Socket, ContentLength),
80+
ok = inet:setopts(Socket, [{packet, http_bin}]),
81+
Body.
82+
83+
respond(Socket, Method, <<"/1">>, _Body) ->
84+
reply(Socket, Method, <<"Hello world!">>);
85+
respond(Socket, Method, <<"/2">>, _Body) ->
86+
reply(Socket, Method, binary:copy(<<"Hello world!">>, 1000));
87+
respond(Socket, Method, <<"/3">>, Body) ->
88+
reply(Socket, Method, Body);
89+
respond(Socket, Method, <<"/4">>, _Body) ->
90+
chunked_reply(Socket, Method, [<<"Hello">>, <<" world!">>]);
91+
respond(Socket, Method, <<"/5">>, _Body) ->
92+
reply(Socket, Method, method(Method)).
93+
94+
method(Method) when is_atom(Method) ->
95+
atom_to_binary(Method, utf8);
96+
method(Method) when is_binary(Method) ->
97+
Method.
98+
99+
reply(Socket, Method, Body) ->
100+
Headers = [
101+
<<"HTTP/1.1 200 OK\r\n">>,
102+
<<"Connection: Keep-Alive\r\n">>,
103+
<<"Content-Type: text/plain\r\n">>,
104+
<<"Content-Length: ">>, integer_to_binary(iolist_size(Body)),
105+
<<"\r\n\r\n">>
106+
],
107+
case Method of
108+
'HEAD' ->
109+
ok = gen_tcp:send(Socket, Headers);
110+
_ ->
111+
ok = gen_tcp:send(Socket, [Headers, Body])
112+
end.
113+
114+
chunked_reply(Socket, Method, Chunks) ->
115+
Headers = [
116+
<<"HTTP/1.1 200 OK\r\n">>,
117+
<<"Connection: Keep-Alive\r\n">>,
118+
<<"Content-Type: text/plain\r\n">>,
119+
<<"Transfer-Encoding: chunked\r\n\r\n">>
120+
],
121+
case Method of
122+
'HEAD' ->
123+
ok = gen_tcp:send(Socket, Headers);
124+
_ ->
125+
Encoded = [[integer_to_binary(byte_size(Chunk), 16), <<"\r\n">>,
126+
Chunk, <<"\r\n">>] || Chunk <- Chunks],
127+
ok = gen_tcp:send(Socket, [Headers, Encoded, <<"0\r\n\r\n">>])
128+
end.

0 commit comments

Comments
 (0)