diff --git a/src/hackney_conn.erl b/src/hackney_conn.erl index 4c65e662..7c3962cd 100644 --- a/src/hackney_conn.erl +++ b/src/hackney_conn.erl @@ -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} -> @@ -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} -> diff --git a/test/hackney_integration_tests.erl b/test/hackney_integration_tests.erl index 002eb2e5..b71d417c 100644 --- a/test/hackney_integration_tests.erl +++ b/test/hackney_integration_tests.erl @@ -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, @@ -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