Skip to content

Commit 1e85121

Browse files
committed
Treat iolist of empty binaries as empty body
Normally when the request body is empty, `handle_body` is only called for POST and PUT requests, since other methods shouldn't have `Content-Type` and `Content-Length` headers for empty bodies. But one case is missed in the empty body detection, an iolist containing empty binaries. The [gleam hackney wrapper](https://github.com/gleam-lang/hackney) represents empty bodies as `[<<>>]`, triggering this bug and causing gleam-lang/hackney#5. This PR expands the empty body detection to check if the result of `iolist_to_binary` is empty.
1 parent 436f7b2 commit 1e85121

File tree

2 files changed

+67
-11
lines changed

2 files changed

+67
-11
lines changed

src/hackney_request.erl

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,18 @@ perform(Client0, {Method0, Path0, Headers0, Body0}) ->
9999
Size, Boundary, Client0);
100100
<<>> when Method =:= <<"POST">> orelse Method =:= <<"PUT">> ->
101101
handle_body(Headers2, ReqType0, Body0, Client0);
102-
[] when Method =:= <<"POST">> orelse Method =:= <<"PUT">> ->
103-
handle_body(Headers2, ReqType0, Body0, Client0);
104102
<<>> ->
105103
{Headers2, ReqType0, Body0, Client0};
106-
[] ->
107-
{Headers2, ReqType0, Body0, Client0};
104+
_ when is_list(Body0) ->
105+
Body1 = iolist_to_binary(Body0),
106+
case Body1 of
107+
<<>> when Method =:= <<"POST">> orelse Method =:= <<"PUT">> ->
108+
handle_body(Headers2, ReqType0, Body1, Client0);
109+
<<>> ->
110+
{Headers2, ReqType0, Body1, Client0};
111+
_ ->
112+
handle_body(Headers2, ReqType0, Body1, Client0)
113+
end;
108114
_ ->
109115
handle_body(Headers2, ReqType0, Body0, Client0)
110116
end,
@@ -370,13 +376,6 @@ handle_body(Headers, ReqType0, Body0, Client) ->
370376
S = hackney_headers_new:get_value(<<"content-length">>, Headers),
371377
{S, CT, Body0};
372378

373-
_ when is_list(Body0) -> % iolist case
374-
Body1 = iolist_to_binary(Body0),
375-
S = erlang:byte_size(Body1),
376-
CT = hackney_headers_new:get_value(
377-
<<"content-type">>, Headers, <<"application/octet-stream">>
378-
),
379-
{S, CT, Body1};
380379
_ when is_binary(Body0) ->
381380
S = erlang:byte_size(Body0),
382381
CT = hackney_headers_new:get_value(

test/hackney_integration_tests.erl

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ all_tests() ->
2020
% fun relative_redirect_request_no_follow/0,
2121
fun relative_redirect_request_follow/0,
2222
fun test_duplicate_headers/0,
23+
fun test_post_includes_content_length_with_body/0,
24+
fun test_post_includes_content_length_with_empty_body/0,
25+
fun test_get_includes_content_length_with_body/0,
26+
fun test_get_excludes_content_length_with_empty_body/0,
2327
fun test_custom_host_headers/0,
2428
fun async_request/0,
2529
fun async_head_request/0,
@@ -186,6 +190,59 @@ test_custom_host_headers() ->
186190
ReqHeaders = proplists:get_value(<<"headers">>, Obj),
187191
?assertEqual(<<"myhost.com">>, proplists:get_value(<<"Host">>, ReqHeaders)).
188192

193+
test_post_includes_content_length_with_body() ->
194+
URL = <<"http://localhost:8000/post">>,
195+
Body = <<"{\"test\": \"ok\" }">>,
196+
Options = [with_body],
197+
{ok, 200, _H, JsonBody} = hackney:post(URL, [], Body, Options),
198+
Obj = jsone:decode(JsonBody, [{object_format, proplist}]),
199+
ReqHeaders = proplists:get_value(<<"headers">>, Obj),
200+
?assertEqual(<<"15">>, proplists:get_value(<<"Content-Length">>, ReqHeaders)).
201+
202+
test_post_includes_content_length_with_empty_body() ->
203+
URL = <<"http://localhost:8000/post">>,
204+
Body = <<>>,
205+
Options = [with_body],
206+
{ok, 200, _H, JsonBody} = hackney:post(URL, [], Body, Options),
207+
Obj = jsone:decode(JsonBody, [{object_format, proplist}]),
208+
ReqHeaders = proplists:get_value(<<"headers">>, Obj),
209+
?assertEqual(<<"0">>, proplists:get_value(<<"Content-Length">>, ReqHeaders)).
210+
211+
test_get_includes_content_length_with_body() ->
212+
URL = <<"http://localhost:8000/post">>,
213+
Body = <<"{\"test\": \"ok\" }">>,
214+
Options = [with_body],
215+
{ok, 200, _H, JsonBody} = hackney:post(URL, [], Body, Options),
216+
Obj = jsone:decode(JsonBody, [{object_format, proplist}]),
217+
ReqHeaders = proplists:get_value(<<"headers">>, Obj),
218+
?assertEqual(<<"15">>, proplists:get_value(<<"Content-Length">>, ReqHeaders)).
219+
%% URL = <<"http://localhost:8000/get">>,
220+
%% TestBodies = [<<>>, [], [<<>>]],
221+
%% Options = [with_body],
222+
%% lists:foreach(fun(Body) ->
223+
%% {ok, 200, _H, JsonBody} = hackney:get(URL, [], Body, Options),
224+
%% Obj = jsone:decode(JsonBody, [{object_format, proplist}]),
225+
%% ReqHeaders = proplists:get_value(<<"headers">>, Obj),
226+
227+
%% %% For GET requests with empty bodies, these headers should not be present
228+
%% ?assertEqual(undefined, proplists:get_value(<<"Content-Type">>, ReqHeaders)),
229+
%% ?assertEqual(undefined, proplists:get_value(<<"Content-Length">>, ReqHeaders))
230+
%% end, TestBodies).
231+
232+
test_get_excludes_content_length_with_empty_body() ->
233+
URL = <<"http://localhost:8000/get">>,
234+
EmptyBodies = [<<>>, [], [<<>>]],
235+
Options = [with_body],
236+
lists:foreach(fun(Body) ->
237+
{ok, 200, _H, JsonBody} = hackney:get(URL, [], Body, Options),
238+
Obj = jsone:decode(JsonBody, [{object_format, proplist}]),
239+
ReqHeaders = proplists:get_value(<<"headers">>, Obj),
240+
241+
%% For GET requests with empty bodies, these headers should not be present
242+
?assertEqual(undefined, proplists:get_value(<<"Content-Type">>, ReqHeaders)),
243+
?assertEqual(undefined, proplists:get_value(<<"Content-Length">>, ReqHeaders))
244+
end, EmptyBodies).
245+
189246
test_frees_manager_ets_when_body_is_in_client() ->
190247
URL = <<"http://localhost:8000/get">>,
191248
BeforeCount = ets:info(hackney_manager_refs, size),

0 commit comments

Comments
 (0)