Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/hackney_conn.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,11 @@ recv_headers(#conn_data{parser = Parser} = Data, Headers) ->
read_full_body(#conn_data{method = <<"HEAD">>} = Data, Acc) ->
%% HEAD requests have no body
{ok, Acc, Data};
read_full_body(#conn_data{status = Status} = Data, Acc) when Status =:= 204; Status =:= 304 ->
%% 204 No Content and 304 Not Modified have no body per RFC 7230 3.3.3
%% Force connection close to avoid corrupting subsequent requests if
%% a misbehaving server sent Content-Length or body data
{ok, Acc, Data#conn_data{socket = undefined}};
read_full_body(Data, Acc) ->
case stream_body_chunk(Data) of
{ok, Chunk, NewData} ->
Expand All @@ -1791,6 +1796,11 @@ read_full_body(Data, Acc) ->
%% @private Stream a single body chunk
stream_body_chunk(#conn_data{method = <<"HEAD">>} = Data) ->
{done, Data};
stream_body_chunk(#conn_data{status = Status} = Data) when Status =:= 204; Status =:= 304 ->
%% 204 No Content and 304 Not Modified have no body per RFC 7230 3.3.3
%% Force connection close to avoid corrupting subsequent requests if
%% a misbehaving server sent Content-Length or body data
{done, Data#conn_data{socket = undefined}};
stream_body_chunk(#conn_data{parser = Parser, transport = Transport, socket = Socket, recv_timeout = Timeout} = Data) ->
case hackney_http:execute(Parser) of
{more, NewParser, _Buffer} ->
Expand Down
29 changes: 29 additions & 0 deletions test/hackney_integration_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ all_tests() ->
fun head_request/0,
fun no_content_response/0,
fun not_modified_response/0,
fun no_content_body_read_then_next_request/0,
fun not_modified_body_read_then_next_request/0,
fun basic_auth_request_failed/0,
fun basic_auth_request/0,
fun basic_auth_url_request/0,
Expand Down Expand Up @@ -85,6 +87,33 @@ not_modified_response() ->
{ok, StatusCode, _, _} = hackney:request(get, URL, [], <<>>, []),
?assertEqual(304, StatusCode).

%% Test for issue #434: 204 response followed by body read should not corrupt next request
%% Per RFC 7230 3.3.3, 204 and 304 responses have no body regardless of headers
no_content_body_read_then_next_request() ->
URL204 = url(<<"/status/204">>),
URLGet = url(<<"/get">>),
%% First request: 204 with explicit body read
{ok, Status1, _, Client1} = hackney:request(get, URL204, [], <<>>, []),
?assertEqual(204, Status1),
{ok, Body1} = hackney:body(Client1),
?assertEqual(<<>>, Body1),
%% Second request: should succeed (connection closed after 204, new connection used)
{ok, Status2, _, _} = hackney:request(get, URLGet, [], <<>>, []),
?assertEqual(200, Status2).

%% Same test for 304 Not Modified
not_modified_body_read_then_next_request() ->
URL304 = url(<<"/status/304">>),
URLGet = url(<<"/get">>),
%% First request: 304 with explicit body read
{ok, Status1, _, Client1} = hackney:request(get, URL304, [], <<>>, []),
?assertEqual(304, Status1),
{ok, Body1} = hackney:body(Client1),
?assertEqual(<<>>, Body1),
%% Second request: should succeed
{ok, Status2, _, _} = hackney:request(get, URLGet, [], <<>>, []),
?assertEqual(200, Status2).

basic_auth_request() ->
URL = url(<<"/basic-auth/username/password">>),
%% Use application variable instead of per-request option
Expand Down
Loading