|
| 1 | +%%% Content-Length on empty request bodies, curl-style: body-bearing methods |
| 2 | +%%% (POST/PUT/PATCH) get an explicit Content-Length: 0; bodyless methods do not, |
| 3 | +%%% and a caller-supplied Content-Length is never duplicated. |
| 4 | +-module(hackney_conn_content_length_tests). |
| 5 | + |
| 6 | +-include_lib("eunit/include/eunit.hrl"). |
| 7 | + |
| 8 | +post_empty_body_gets_content_length_zero_test() -> |
| 9 | + Req = capture(<<"POST">>, [], <<>>), |
| 10 | + ?assertNotEqual(nomatch, cl(Req, <<"0">>)). |
| 11 | + |
| 12 | +put_empty_body_gets_content_length_zero_test() -> |
| 13 | + Req = capture(<<"PUT">>, [], <<>>), |
| 14 | + ?assertNotEqual(nomatch, cl(Req, <<"0">>)). |
| 15 | + |
| 16 | +get_empty_body_has_no_content_length_test() -> |
| 17 | + Req = capture(<<"GET">>, [], <<>>), |
| 18 | + ?assertEqual(nomatch, binary:match(lower(Req), <<"content-length:">>)). |
| 19 | + |
| 20 | +delete_empty_body_has_no_content_length_test() -> |
| 21 | + Req = capture(<<"DELETE">>, [], <<>>), |
| 22 | + ?assertEqual(nomatch, binary:match(lower(Req), <<"content-length:">>)). |
| 23 | + |
| 24 | +user_content_length_not_duplicated_test() -> |
| 25 | + Req = capture(<<"POST">>, [{<<"Content-Length">>, <<"5">>}], <<>>), |
| 26 | + %% Exactly one content-length header, and it is the caller's value. |
| 27 | + Matches = binary:matches(lower(Req), <<"content-length:">>), |
| 28 | + ?assertEqual(1, length(Matches)), |
| 29 | + ?assertNotEqual(nomatch, cl(Req, <<"5">>)). |
| 30 | + |
| 31 | +%% Find "content-length: <V>" case-insensitively. |
| 32 | +cl(Req, V) -> |
| 33 | + binary:match(lower(Req), <<"content-length: ", V/binary, "\r\n">>). |
| 34 | + |
| 35 | +lower(Bin) -> hackney_bstr:to_lower(Bin). |
| 36 | + |
| 37 | +%% Drive a real buffered request into a raw listener and return the request |
| 38 | +%% bytes the server received. |
| 39 | +capture(Method, Headers, Body) -> |
| 40 | + {ok, _} = application:ensure_all_started(hackney), |
| 41 | + {ok, LSock} = gen_tcp:listen(0, [binary, {active, false}, |
| 42 | + {reuseaddr, true}, {ip, {127, 0, 0, 1}}]), |
| 43 | + {ok, Port} = inet:port(LSock), |
| 44 | + Parent = self(), |
| 45 | + spawn(fun() -> |
| 46 | + case gen_tcp:accept(LSock, 3000) of |
| 47 | + {ok, S} -> |
| 48 | + ReqBytes = recv_headers(S, <<>>), |
| 49 | + gen_tcp:send(S, <<"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n">>), |
| 50 | + Parent ! {captured, ReqBytes}, |
| 51 | + gen_tcp:close(S); |
| 52 | + _ -> Parent ! {captured, <<>>} |
| 53 | + end, |
| 54 | + gen_tcp:close(LSock) |
| 55 | + end), |
| 56 | + {ok, Pid} = hackney_conn:start_link(#{host => "127.0.0.1", port => Port, |
| 57 | + transport => hackney_tcp, |
| 58 | + connect_timeout => 1000}), |
| 59 | + ok = hackney_conn:connect(Pid, 1000), |
| 60 | + {ok, 200, _} = hackney_conn:request(Pid, Method, <<"/">>, Headers, Body), |
| 61 | + catch hackney_conn:stop(Pid), |
| 62 | + receive {captured, Req} -> Req after 3000 -> error(no_capture) end. |
| 63 | + |
| 64 | +recv_headers(S, Acc) -> |
| 65 | + case binary:match(Acc, <<"\r\n\r\n">>) of |
| 66 | + nomatch -> |
| 67 | + case gen_tcp:recv(S, 0, 2000) of |
| 68 | + {ok, D} -> recv_headers(S, <<Acc/binary, D/binary>>); |
| 69 | + {error, _} -> Acc |
| 70 | + end; |
| 71 | + _ -> Acc |
| 72 | + end. |
0 commit comments