157157 request_from :: {pid (), reference ()} | undefined ,
158158 method :: binary () | undefined ,
159159 path :: binary () | undefined ,
160+ % % Whether the current request carried Connection: close (the caller asked
161+ % % the server to close). Folded into the keepalive decision at checkin so a
162+ % % requested close is never reused. Reset on every request send.
163+ request_close = false :: boolean (),
160164
161165 % % Response state
162166 version :: {integer (), integer ()} | undefined ,
@@ -583,9 +587,12 @@ is_no_reuse(Pid) ->
583587
584588% % @doc Get the flags the pool needs for its checkin decision in one call:
585589% % whether the connection was SSL upgraded, opted into SSL pooling, must not
586- % % be reused (proxy tunnels), and the negotiated protocol.
590+ % % be reused (proxy tunnels), the negotiated protocol, whether the response
591+ % % requires the connection to close (keepalive), and whether the socket is
592+ % % proven ready to pool.
587593-spec checkin_info (pid ()) -> #{upgraded_ssl := boolean (), no_reuse := boolean (),
588- pool_ssl := boolean (), protocol := atom ()}.
594+ pool_ssl := boolean (), protocol := atom (),
595+ should_close := boolean (), ready := boolean ()}.
589596checkin_info (Pid ) ->
590597 gen_statem :call (Pid , checkin_info ).
591598
@@ -921,7 +928,9 @@ connected({call, From}, is_no_reuse, #conn_data{no_reuse = NoReuse}) ->
921928 {keep_state_and_data , [{reply , From , NoReuse }]};
922929
923930connected ({call , From }, checkin_info , Data ) ->
924- {keep_state_and_data , [{reply , From , checkin_info_map (Data )}]};
931+ % % In connected state: prove the socket is ready before the pool may keep it.
932+ Map = (checkin_info_map (Data ))#{ready => socket_ready (Data )},
933+ {keep_state_and_data , [{reply , From , Map }]};
925934
926935connected ({call , From }, {request , Method , Path , Headers , Body , ReqOpts }, # conn_data {protocol = http2 } = Data ) ->
927936 % % HTTP/2 request - use h2_machine (1xx not applicable for HTTP/2)
@@ -1232,10 +1241,15 @@ streaming_body(internal, {send_headers_only, Method, Path, Headers}, Data) ->
12321241 RequestLine = build_request_line (Method , Path ),
12331242 HeaderLines = [[Name , <<" : " >>, Value , <<" \r\n " >>] || {Name , Value } <- HeadersList ],
12341243 HeadersData = [RequestLine , HeaderLines , <<" \r\n " >>],
1244+ % % Record whether the caller asked the server to close (the request line is
1245+ % % built here, not via do_send_request/5), for the keepalive decision.
1246+ RequestClose = hackney_keepalive :request_closes (HeadersWithTE ),
12351247 case Transport :send (Socket , HeadersData ) of
12361248 ok ->
12371249 From = Data # conn_data .request_from ,
1238- {keep_state , Data # conn_data {request_from = undefined }, [{reply , From , ok }]};
1250+ {keep_state , Data # conn_data {request_from = undefined ,
1251+ request_close = RequestClose },
1252+ [{reply , From , ok }]};
12391253 {error , Reason } ->
12401254 From = Data # conn_data .request_from ,
12411255 {next_state , closed , Data , [{reply , From , {error , Reason }}]}
@@ -1840,7 +1854,9 @@ handle_common({call, From}, is_no_reuse, _State, #conn_data{no_reuse = NoReuse})
18401854 {keep_state_and_data , [{reply , From , NoReuse }]};
18411855
18421856handle_common ({call , From }, checkin_info , _State , Data ) ->
1843- {keep_state_and_data , [{reply , From , checkin_info_map (Data )}]};
1857+ % % Any non-connected state (e.g. already closed) is not poolable.
1858+ Map = (checkin_info_map (Data ))#{ready => false },
1859+ {keep_state_and_data , [{reply , From , Map }]};
18441860
18451861handle_common ({call , From }, get_protocol , _State , # conn_data {protocol = Protocol }) ->
18461862 {keep_state_and_data , [{reply , From , Protocol }]};
@@ -1894,14 +1910,18 @@ reset_async(Data) ->
18941910 stream_to = undefined
18951911 }.
18961912
1897- % % @private Check if connection should be closed based on response headers
1898- should_close_connection (# conn_data {response_headers = undefined }) ->
1913+ % % @private Whether the connection must close (not be reused) for keepalive
1914+ % % reasons. Distinguishes "no response observed yet" (a fresh or idle pooled
1915+ % % conn, which stays poolable) from a parsed response, whose close decision
1916+ % % follows RFC 7230 via hackney_keepalive:should_close/3.
1917+ should_close_connection (# conn_data {response_headers = undefined , version = undefined }) ->
1918+ % % No request/response cycle has run on this connection: keep it poolable so
1919+ % % a plain checkout/checkin (no request) still returns the live conn to the
1920+ % % pool.
18991921 false ;
1900- should_close_connection (# conn_data {response_headers = Headers }) ->
1901- case hackney_headers :get_value (<<" connection" >>, Headers ) of
1902- undefined -> false ;
1903- Value -> hackney_bstr :to_lower (Value ) =:= <<" close" >>
1904- end .
1922+ should_close_connection (# conn_data {version = Version , response_headers = Headers ,
1923+ request_close = RequestClose }) ->
1924+ hackney_keepalive :should_close (Version , Headers , RequestClose ).
19051925
19061926% % @private Finish async streaming - close or return to connected based on Connection header
19071927finish_async_streaming (Data ) ->
@@ -1960,6 +1980,10 @@ do_send_request(Method, Path, Headers, Body, Data) ->
19601980 % % Build request headers
19611981 FinalHeaders = build_headers (Method , Headers , Body , Netloc ),
19621982
1983+ % % Record whether the caller asked the server to close, for the keepalive
1984+ % % decision at checkin. Reset per request.
1985+ Data1 = Data # conn_data {request_close = hackney_keepalive :request_closes (FinalHeaders )},
1986+
19631987 % % Build request line and headers
19641988 Path1 = case Path of
19651989 <<>> -> <<" /" >>;
@@ -1975,7 +1999,7 @@ do_send_request(Method, Path, Headers, Body, Data) ->
19751999 % % Send body if present
19762000 case send_body (Transport , Socket , Body ) of
19772001 ok ->
1978- {ok , Data };
2002+ {ok , Data1 };
19792003 {error , Reason } ->
19802004 {error , Reason }
19812005 end ;
@@ -2428,6 +2452,16 @@ has_pending_close(Socket) ->
24282452 false
24292453 end .
24302454
2455+ % % @private Whether the socket is proven ready to pool: present, no queued close,
2456+ % % and still connected. Runs in the conn process (peername + receive-after-0,
2457+ % % both fast); consuming a queued close here makes a server-closed idle conn
2458+ % % report not-ready. Does not disable active-once, so no stranded-byte regression.
2459+ socket_ready (# conn_data {socket = undefined }) ->
2460+ false ;
2461+ socket_ready (# conn_data {transport = Transport , socket = Socket }) ->
2462+ not has_pending_close (Socket ) andalso
2463+ check_socket_health (Transport , Socket ) =:= ok .
2464+
24312465% % @private Drain and return any {tcp/ssl, Socket, Data} bytes queued in the
24322466% % mailbox. #544 puts idle pooled sockets in {active, once}, which delivers the
24332467% % next inbound bytes (the start of the response on a reused connection) as a
@@ -2478,16 +2512,24 @@ notify_pool_available_sync(#conn_data{pool_pid = undefined}) ->
24782512 ok ;
24792513notify_pool_available_sync (# conn_data {pool_pid = PoolPid , upgraded_ssl = UpgradedSsl ,
24802514 no_reuse = NoReuse , pool_ssl = PoolSsl ,
2481- protocol = Protocol }) ->
2515+ protocol = Protocol } = Data ) ->
2516+ % % do_checkin_with_close_flag/3 pools whenever this flag is false, with no
2517+ % % further check, so also close on a keepalive-close response and on an
2518+ % % unready socket - matching the async-cast checkin gate.
24822519 ShouldClose = NoReuse orelse (UpgradedSsl andalso not PoolSsl )
2483- orelse Protocol =/= http1 ,
2520+ orelse Protocol =/= http1
2521+ orelse should_close_connection (Data )
2522+ orelse not socket_ready (Data ),
24842523 gen_server :call (PoolPid , {checkin_sync , self (), ShouldClose }, 5000 ).
24852524
2486- % % @private Flags for the pool's checkin decision, see checkin_info/1.
2525+ % % @private Flags for the pool's checkin decision, see checkin_info/1. The
2526+ % % `ready' flag is added per-state by the checkin_info handlers, since it
2527+ % % depends on the conn being in `connected' with a healthy socket.
24872528checkin_info_map (# conn_data {upgraded_ssl = UpgradedSsl , no_reuse = NoReuse ,
2488- pool_ssl = PoolSsl , protocol = Protocol }) ->
2529+ pool_ssl = PoolSsl , protocol = Protocol } = Data ) ->
24892530 #{upgraded_ssl => UpgradedSsl , no_reuse => NoReuse ,
2490- pool_ssl => PoolSsl , protocol => Protocol }.
2531+ pool_ssl => PoolSsl , protocol => Protocol ,
2532+ should_close => should_close_connection (Data )}.
24912533
24922534% % @private Start an async request
24932535do_request_async (From , Method , Path , Headers , Body , AsyncMode , StreamTo , FollowRedirect , Data ) ->
0 commit comments