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:21:56 UTC

(couchdb-gun) 06/16: Do not crash gun_pool on HTTP/2 server-pushed streams

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 1fe90ade5a996878e09481eb93cb975f3babe6c5
Author: Loïc Hoguin <es...@ninenines.eu>
AuthorDate: Sat Sep 26 12:30:57 2026 +0200

    Do not crash gun_pool on HTTP/2 server-pushed streams
    
    A pushed stream calls response_end on a ref that never passed
    request_start. That event had no clause and crashed the
    connection. An unknown ref leaves the stream counter unchanged.
---
 src/gun_pool_events_h.erl |  6 +++++-
 test/pool_SUITE.erl       | 40 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/src/gun_pool_events_h.erl b/src/gun_pool_events_h.erl
index 3bae13a..ba27543 100644
--- a/src/gun_pool_events_h.erl
+++ b/src/gun_pool_events_h.erl
@@ -107,7 +107,11 @@ response_end(Event=#{stream_ref := StreamRef}, State0=#{table := Tid}) ->
 			_ = ets:update_counter(Tid, self(), -1),
 			maps:remove(StreamRef, State0);
 		#{StreamRef := {IsFin, nofin}} ->
-			State0#{StreamRef => {IsFin, fin}}
+			State0#{StreamRef => {IsFin, fin}};
+		%% A pushed stream calls response_end without request_start.
+		%% The ref is unknown, so the counter stays unchanged.
+		_ ->
+			State0
 	end,
 	propagate(Event, State, ?FUNCTION_NAME).
 
diff --git a/test/pool_SUITE.erl b/test/pool_SUITE.erl
index a125a09..610191a 100644
--- a/test/pool_SUITE.erl
+++ b/test/pool_SUITE.erl
@@ -41,6 +41,8 @@ do_proto_opts() ->
 	Routes = [
 		{"/", hello_h, []},
 		{"/delay", delayed_hello_h, 3000},
+		{"/empty", empty_h, []},
+		{"/push", push_h, []},
 		{"/ws", ws_echo_h, []}
 	],
 	#{
@@ -375,6 +377,44 @@ reconnect_h2(Config) ->
 
 %% @todo reconnect_ws
 
+push_promise_no_crash(Config) ->
+	doc("Confirm a server-pushed HTTP/2 stream does not crash "
+		"the pooled connection or change its stream counter."),
+	Port = config(port, Config),
+	Authority = ["localhost:", integer_to_binary(Port)],
+	{ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{
+		conn_opts => #{protocols => [http2]},
+		scope => ?FUNCTION_NAME,
+		size => 1
+	}),
+	gun_pool:await_up(ManagerPid),
+	{async, PoolRef={ConnPid, _}} = gun_pool:get("/push",
+		#{<<"host">> => Authority},
+		#{scope => ?FUNCTION_NAME}),
+	{push, PromisedRef1, <<"GET">>, _, _} = gun_pool:await(PoolRef),
+	{push, PromisedRef2, <<"GET">>, _, _} = gun_pool:await(PoolRef),
+	{response, nofin, 200, _} = gun_pool:await(PoolRef),
+	{ok, <<"Hello world!">>} = gun_pool:await_body(PoolRef),
+	ok = do_await_pushed({ConnPid, PromisedRef1}),
+	ok = do_await_pushed({ConnPid, PromisedRef2}),
+	{operational, #{table := Tid, conns := Conns}} = gun_pool:info(ManagerPid),
+	{up, http2, _} = maps:get(ConnPid, Conns),
+	[{_, 0}] = ets:lookup(Tid, ConnPid),
+	{async, NextRef} = gun_pool:get("/",
+		#{<<"host">> => Authority},
+		#{scope => ?FUNCTION_NAME}),
+	{response, nofin, 200, _} = gun_pool:await(NextRef),
+	{ok, <<"Hello world!">>} = gun_pool:await_body(NextRef).
+
+do_await_pushed(PoolRef) ->
+	case gun_pool:await(PoolRef) of
+		{response, fin, 200, _} ->
+			ok;
+		{response, nofin, 200, _} ->
+			{ok, _} = gun_pool:await_body(PoolRef),
+			ok
+	end.
+
 stop_pool(Config) ->
 	doc("Confirm the pool can be stopped."),
 	Port = config(port, Config),