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

openSUSE Build Service is sponsored by