File 0725-inets-Do-not-send-Content-Length-header-for-bodyless.patch of Package erlang
From 702557b513a69547acb259ad2bf36fd27dab560a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= <jonatan@maennchen.ch>
Date: Thu, 8 Jan 2026 15:22:31 +0100
Subject: [PATCH] inets: Do not send Content-Length header for bodyless
requests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
RFC 9110 states: "A user agent SHOULD NOT send a Content-Length
header field when the request message does not contain content
and the method semantics do not anticipate such data."
Previously, httpc sent Content-Length: 0 for all requests including
GET, HEAD, OPTIONS, TRACE, and DELETE without body. This caused
issues with strict HTTP validators like AWS Elastic Load Balancers.
Now, Content-Length is omitted for these methods when no body is
provided in the request.
Fixes https://github.com/erlang/otp/issues/10513
Signed-off-by: Jonatan Männchen <jonatan@maennchen.ch>
---
lib/inets/src/http_client/httpc_request.erl | 11 +++
lib/inets/test/httpc_SUITE.erl | 78 ++++++++++++++++++++-
2 files changed, 88 insertions(+), 1 deletion(-)
diff --git a/lib/inets/src/http_client/httpc_request.erl b/lib/inets/src/http_client/httpc_request.erl
index f57bbf470a..d518be690d 100644
--- a/lib/inets/src/http_client/httpc_request.erl
+++ b/lib/inets/src/http_client/httpc_request.erl
@@ -198,6 +198,17 @@ is_client_closing(Headers) ->
%%%========================================================================
%%% Internal functions
%%%========================================================================
+post_data(Method, Headers, {[], []}, [])
+ when Method =:= get;
+ Method =:= head;
+ Method =:= options;
+ Method =:= trace;
+ Method =:= delete ->
+ %% RFC 9110: A user agent SHOULD NOT send a Content-Length header field
+ %% when the request message does not contain content and the method
+ %% semantics do not anticipate such data.
+ {Headers#http_request_h{'content-length' = undefined}, ""};
+
post_data(Method, Headers, {ContentType, Body}, HeadersAsIs)
when (Method =:= post)
orelse (Method =:= put)
diff --git a/lib/inets/test/httpc_SUITE.erl b/lib/inets/test/httpc_SUITE.erl
index 5d3df21659..daf406da0d 100644
--- a/lib/inets/test/httpc_SUITE.erl
+++ b/lib/inets/test/httpc_SUITE.erl
@@ -209,7 +209,10 @@ only_simulated() ->
get_space,
delete_no_body,
post_with_content_type,
- stream_fun_server_close
+ stream_fun_server_close,
+ no_content_length_for_bodyless_requests,
+ content_length_for_empty_body_requests,
+ content_length_via_headers_as_is
].
server_closing_connection() ->
@@ -2114,6 +2117,54 @@ post_with_content_type(Config) when is_list(Config) ->
{ok, {{_,500,_}, _, _}} =
httpc:request(post, {URL, [], "application/x-www-form-urlencoded", ""}, [?SSL_NO_VERIFY], RequestOpts).
+%%--------------------------------------------------------------------
+no_content_length_for_bodyless_requests() ->
+ [{doc, "Test that bodyless requests (GET, HEAD, OPTIONS, TRACE, DELETE) "
+ "do not send Content-Length header (RFC 9110)"}].
+no_content_length_for_bodyless_requests(Config) when is_list(Config) ->
+ URL = url(group_name(Config), "/check_no_content_length.html", Config),
+ Profile = ?profile(Config),
+ %% Simulated server replies 500 if Content-Length header is present
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(get, {URL, []}, [?SSL_NO_VERIFY], [], Profile),
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(head, {URL, []}, [?SSL_NO_VERIFY], [], Profile),
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(options, {URL, []}, [?SSL_NO_VERIFY], [], Profile),
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(trace, {URL, []}, [?SSL_NO_VERIFY], [], Profile),
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(delete, {URL, []}, [?SSL_NO_VERIFY], [], Profile).
+
+%%--------------------------------------------------------------------
+content_length_for_empty_body_requests() ->
+ [{doc, "Test that POST/PUT with empty body DOES send Content-Length: 0 (RFC 9110)"}].
+content_length_for_empty_body_requests(Config) when is_list(Config) ->
+ URL = url(group_name(Config), "/check_has_content_length_zero.html", Config),
+ Profile = ?profile(Config),
+ %% Simulated server replies 500 if Content-Length header is NOT present or not "0"
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(post, {URL, [], "text/plain", ""}, [?SSL_NO_VERIFY], [], Profile),
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(put, {URL, [], "text/plain", ""}, [?SSL_NO_VERIFY], [], Profile).
+
+%%--------------------------------------------------------------------
+content_length_via_headers_as_is() ->
+ [{doc, "Test that explicit Content-Length via headers_as_is is respected "
+ "for bodyless requests"}].
+content_length_via_headers_as_is(Config) when is_list(Config) ->
+ URL = url(group_name(Config), "/check_has_content_length_zero.html", Config),
+ URLNoContentLength = url(group_name(Config), "/check_no_content_length.html", Config),
+ %% User explicitly sets Content-Length: 0 via headers_as_is - should be sent
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(get, {URL, [{"Host", "localhost"}, {"Content-Length", "0"}]},
+ [?SSL_NO_VERIFY], [{headers_as_is, true}], ?profile(Config)),
+ %% User provides custom header but NOT Content-Length, without headers_as_is
+ %% - Content-Length should still be omitted
+ {ok, {{_,200,_}, _, _}} =
+ httpc:request(get, {URLNoContentLength, [{"X-Custom-Header", "value"}]},
+ [?SSL_NO_VERIFY], [], ?profile(Config)).
+
%%--------------------------------------------------------------------
request_options() ->
[{require, ipv6_hosts},
@@ -2624,6 +2675,13 @@ content_type_header([{"content-type", Value}|_]) ->
content_type_header([_|T]) ->
content_type_header(T).
+content_length_header([]) ->
+ not_found;
+content_length_header([{"content-length", Value}|_]) ->
+ {ok, string:strip(Value)};
+content_length_header([_|T]) ->
+ content_length_header(T).
+
handle_auth("Basic " ++ UserInfo, Challenge, DefaultResponse) ->
case string:tokens(base64:decode_to_string(UserInfo), ":") of
["alladin@example.com", "sesame"] = Auth ->
@@ -3227,6 +3285,24 @@ handle_uri(_,"/delete_no_body.html", _,Headers,_, DefaultResponse) ->
not_found ->
DefaultResponse
end;
+handle_uri(_,"/check_no_content_length.html", _,Headers,_, DefaultResponse) ->
+ Error = "HTTP/1.1 500 Internal Server Error\r\n" ++
+ "Content-Length:0\r\n\r\n",
+ case content_length_header(Headers) of
+ {ok, _} ->
+ Error;
+ not_found ->
+ DefaultResponse
+ end;
+handle_uri(_,"/check_has_content_length_zero.html", _,Headers,_, DefaultResponse) ->
+ Error = "HTTP/1.1 500 Internal Server Error\r\n" ++
+ "Content-Length:0\r\n\r\n",
+ case content_length_header(Headers) of
+ {ok, "0"} ->
+ DefaultResponse;
+ _ ->
+ Error
+ end;
handle_uri(_,_,_,_,_,DefaultResponse) ->
DefaultResponse.
--
2.51.0