You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2026/10/05 23:22:05 UTC
(couchdb-gun) 15/16: Reject responses with conflicting content-length
This is an automated email from the ASF dual-hosted git repository.
nickva pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-gun.git
commit a3ae11a08cbe0ee44be98acb3146b5fc35a612fa
Author: Loïc Hoguin <es...@ninenines.eu>
AuthorDate: Fri Sep 25 18:06:12 2026 +0200
Reject responses with conflicting content-length
---
src/gun_http.erl | 52 +++++++++++++++++++++++----------
test/rfc7230_SUITE.erl | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+), 16 deletions(-)
diff --git a/src/gun_http.erl b/src/gun_http.erl
index fb1cec3..5e0fecd 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -354,7 +354,8 @@ match_header_section_end(Data, _) ->
end.
handle_head(Data, State=#http_state{opts=Opts,
- streams=[#stream{ref=StreamRef, reply_to=ReplyTo, authority=Authority, path=Path}|_]},
+ streams=[#stream{ref=StreamRef, reply_to=ReplyTo, method=Method,
+ authority=Authority, path=Path}|_]},
CookieStore0, EvHandler, EvHandlerState) ->
try cow_http:parse_status_line(Data) of
{Version, Status, _, Rest0} ->
@@ -368,15 +369,27 @@ handle_head(Data, State=#http_state{opts=Opts,
gun:reply(ReplyTo, {gun_error, self(), Reason}),
{{error, Reason}, CookieStore0, EvHandlerState};
false ->
- CookieStore = gun_cookies:set_cookie_header(scheme(State),
- Authority, Path, Status, Headers, CookieStore0, Opts),
case StreamRef of
{connect, _, _} when Status >= 200, Status < 300 ->
+ CookieStore = gun_cookies:set_cookie_header(scheme(State),
+ Authority, Path, Status, Headers, CookieStore0, Opts),
handle_connect(Rest, State, CookieStore, EvHandler, EvHandlerState, Status, Headers);
_ when Status >= 100, Status =< 199 ->
+ CookieStore = gun_cookies:set_cookie_header(scheme(State),
+ Authority, Path, Status, Headers, CookieStore0, Opts),
handle_inform(Rest, State, CookieStore, EvHandler, EvHandlerState, Version, Status, Headers);
_ ->
- handle_response(Rest, State, CookieStore, EvHandler, EvHandlerState, Version, Status, Headers)
+ case response_io_from_headers(Method, Version, Status, Headers) of
+ {error, Human} ->
+ Reason = {connection_error, protocol_error, Human},
+ gun:reply(ReplyTo, {gun_error, self(), Reason}),
+ {{error, Reason}, CookieStore0, EvHandlerState};
+ In ->
+ CookieStore = gun_cookies:set_cookie_header(scheme(State),
+ Authority, Path, Status, Headers, CookieStore0, Opts),
+ handle_response1(Rest, State, CookieStore, EvHandler,
+ EvHandlerState, Version, Status, Headers, In)
+ end
end
end
catch _:_ ->
@@ -494,10 +507,9 @@ is_expected_upgrade_response(Headers, #stream{upgrade=Requested}) ->
false
end.
-handle_response(Rest, State=#http_state{version=ClientVersion, opts=Opts, connection=Conn,
- streams=[Stream=#stream{ref=StreamRef, reply_to=ReplyTo, method=Method, is_alive=IsAlive}|Tail]},
- CookieStore, EvHandler, EvHandlerState0, Version, Status, Headers) ->
- In = response_io_from_headers(Method, Version, Status, Headers),
+handle_response1(Rest, State=#http_state{version=ClientVersion, opts=Opts, connection=Conn,
+ streams=[Stream=#stream{ref=StreamRef, reply_to=ReplyTo, is_alive=IsAlive}|Tail]},
+ CookieStore, EvHandler, EvHandlerState0, Version, Status, Headers, In) ->
IsFin = case In of head -> fin; _ -> nofin end,
RealStreamRef = stream_ref(State, StreamRef),
%% @todo Figure out whether the event should trigger if the stream was cancelled.
@@ -1039,14 +1051,22 @@ response_io_from_headers(_, Version, _Status, Headers) ->
[<<"identity">>] -> body_close
end;
_ ->
- case lists:keyfind(<<"content-length">>, 1, Headers) of
- {_, <<"0">>} ->
- head;
- {_, Length} ->
- {body, cow_http_hd:parse_content_length(Length)};
- _ ->
- body_close
- end
+ response_io_from_content_length(Headers)
+ end.
+
+response_io_from_content_length(Headers) ->
+ case lists:usort([Value || {<<"content-length">>, Value} <- Headers]) of
+ [] ->
+ body_close;
+ [Value] ->
+ try cow_http_hd:parse_content_length(Value) of
+ 0 -> head;
+ Len -> {body, Len}
+ catch _:_ ->
+ {error, "The content-length header is invalid."}
+ end;
+ _ ->
+ {error, "The content-length header is invalid."}
end.
%% Streams.
diff --git a/test/rfc7230_SUITE.erl b/test/rfc7230_SUITE.erl
index 6679c96..4c36c3c 100644
--- a/test/rfc7230_SUITE.erl
+++ b/test/rfc7230_SUITE.erl
@@ -26,6 +26,85 @@ all() ->
%% Tests.
+content_length_duplicate_error(_) ->
+ doc("Multiple differing content-length values must be rejected "
+ "with a protocol error connection error. (RFC9112 6.3)"),
+ {ok, _, OriginPort} = init_origin(tcp, http,
+ fun(_, _, ClientSocket, ClientTransport) ->
+ {ok, _} = ClientTransport:recv(ClientSocket, 0, 1000),
+ ClientTransport:send(ClientSocket,
+ "HTTP/1.1 200 OK\r\n"
+ "content-length: 4\r\n"
+ "content-length: 40\r\n"
+ "\r\n"
+ )
+ end),
+ {ok, ConnPid} = gun:open("localhost", OriginPort),
+ {ok, http} = gun:await_up(ConnPid),
+ StreamRef = gun:get(ConnPid, "/"),
+ {error, {connection_error, {connection_error, protocol_error, _}}} =
+ gun:await(ConnPid, StreamRef),
+ gun:close(ConnPid).
+
+content_length_equivalent_duplicate_error(_) ->
+ doc("Multiple differing content-length values must be rejected "
+ "with a protocol error connection error. (RFC9112 6.3)"),
+ {ok, _, OriginPort} = init_origin(tcp, http,
+ fun(_, _, ClientSocket, ClientTransport) ->
+ {ok, _} = ClientTransport:recv(ClientSocket, 0, 1000),
+ ClientTransport:send(ClientSocket,
+ "HTTP/1.1 200 OK\r\n"
+ "content-length: 0\r\n"
+ "content-length: 00\r\n"
+ "\r\n"
+ )
+ end),
+ {ok, ConnPid} = gun:open("localhost", OriginPort),
+ {ok, http} = gun:await_up(ConnPid),
+ StreamRef = gun:get(ConnPid, "/"),
+ {error, {connection_error, {connection_error, protocol_error, _}}} =
+ gun:await(ConnPid, StreamRef),
+ gun:close(ConnPid).
+
+content_length_duplicate_same_value(_) ->
+ doc("Identical repeated content-length values must be tolerated. (RFC9112 6.3)"),
+ {ok, _, OriginPort} = init_origin(tcp, http,
+ fun(_, _, ClientSocket, ClientTransport) ->
+ {ok, _} = ClientTransport:recv(ClientSocket, 0, 1000),
+ ClientTransport:send(ClientSocket,
+ "HTTP/1.1 200 OK\r\n"
+ "content-length: 4\r\n"
+ "content-length: 4\r\n"
+ "\r\n"
+ "data"
+ )
+ end),
+ {ok, ConnPid} = gun:open("localhost", OriginPort),
+ {ok, http} = gun:await_up(ConnPid),
+ StreamRef = gun:get(ConnPid, "/"),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
+ {ok, <<"data">>} = gun:await_body(ConnPid, StreamRef),
+ gun:close(ConnPid).
+
+content_length_unparseable_error(_) ->
+ doc("An unparseable content-length value must be rejected "
+ "with a protocol error connection error. (RFC9112 6.3)"),
+ {ok, _, OriginPort} = init_origin(tcp, http,
+ fun(_, _, ClientSocket, ClientTransport) ->
+ {ok, _} = ClientTransport:recv(ClientSocket, 0, 1000),
+ ClientTransport:send(ClientSocket,
+ "HTTP/1.1 200 OK\r\n"
+ "content-length: abc\r\n"
+ "\r\n"
+ )
+ end),
+ {ok, ConnPid} = gun:open("localhost", OriginPort),
+ {ok, http} = gun:await_up(ConnPid),
+ StreamRef = gun:get(ConnPid, "/"),
+ {error, {connection_error, {connection_error, protocol_error, _}}} =
+ gun:await(ConnPid, StreamRef),
+ gun:close(ConnPid).
+
host_default_port_http(_) ->
doc("The default port for http should not be sent in the host header. (RFC7230 2.7.1)"),
do_host_port(tcp, 80, <<>>).