Skip to content

Commit cb202ac

Browse files
committed
Reply with {error, closed} instead of {error, illegal_state} on calls to closed connection
This fixes #932
1 parent 9928f3c commit cb202ac

3 files changed

Lines changed: 17 additions & 7 deletions

File tree

src/hackney_conn.erl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,8 +1824,8 @@ closed(enter, _OldState, #conn_data{socket = Socket, transport = Transport, pool
18241824
%% late-arriving {call, From, {request, _}} messages from workers that
18251825
%% raced the pool checkout race a terminating gen_statem — which
18261826
%% surfaces as `exit:{normal, _}` in the caller (issue #836). Stay
1827-
%% alive briefly so those late calls get a proper `{error, {closed, _}}`
1828-
%% reply via handle_common's invalid_state fallback, then stop.
1827+
%% alive briefly so those late calls get a proper `{error, closed}`
1828+
%% reply via the dedicated closed/3 catch-all clause below, then stop.
18291829
case PoolPid of
18301830
undefined ->
18311831
{keep_state, Data#conn_data{socket = undefined}};
@@ -1879,6 +1879,14 @@ closed(cast, {set_owner, _NewOwner}, #conn_data{pool_pid = PoolPid} = Data)
18791879
%% of lingering through the grace window and being handed out again.
18801880
{stop, normal, Data};
18811881

1882+
closed({call, From}, _Msg, _Data) ->
1883+
%% Any other synchronous call arriving during the grace window (request,
1884+
%% request_async, send_headers, body, stream_body, etc.) gets a proper
1885+
%% `{error, closed}` instead of the generic `{error, invalid_state}` from
1886+
%% handle_common. This lets callers distinguish a peer-closed connection
1887+
%% from a misuse of the API.
1888+
{keep_state_and_data, [{reply, From, {error, closed}}]};
1889+
18821890
closed(EventType, Event, Data) ->
18831891
handle_common(EventType, Event, closed, Data).
18841892

src/hackney_pool.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,10 +1127,12 @@ set_owner(Pid, Owner) ->
11271127
end.
11281128

11291129
%% @private Fetch the conn's checkin flags, or `error' if the call fails (the
1130-
%% conn died between is_process_alive/1 and here). Caller treats `error' as
1131-
%% not poolable.
1130+
%% conn died between is_process_alive/1 and here) or about to close (in grace period).
1131+
%% Caller treats `error' as not poolable.
11321132
checkin_info(Pid) ->
1133-
try {ok, hackney_conn:checkin_info(Pid, ?PROBE_TIMEOUT)}
1133+
try hackney_conn:checkin_info(Pid, ?PROBE_TIMEOUT) of
1134+
Info when is_map(Info) -> {ok, Info};
1135+
_ -> error
11341136
catch _:_ -> error
11351137
end.
11361138

test/hackney_conn_tests.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,15 +256,15 @@ test_owner_death() ->
256256

257257
%% #850: when a checkout races a server-side close, the pool calls set_owner on
258258
%% a connection that has just transitioned to `closed`. It must get
259-
%% {error, invalid_state} back (so the pool can fall through to a fresh
259+
%% {error, closed} back (so the pool can fall through to a fresh
260260
%% connection) rather than crash. A non-pooled connection has no grace timer,
261261
%% so it stays in `closed` to answer.
262262
test_set_owner_closed_returns_error() ->
263263
{Pid, ListenSock} = connected_conn(#{}),
264264
?assertEqual({ok, connected}, hackney_conn:get_state(Pid)),
265265
ok = hackney_conn:close(Pid),
266266
?assertEqual({ok, closed}, hackney_conn:get_state(Pid)),
267-
?assertEqual({error, invalid_state}, hackney_conn:set_owner(Pid, self())),
267+
?assertEqual({error, closed}, hackney_conn:set_owner(Pid, self())),
268268
hackney_conn:stop(Pid),
269269
gen_tcp:close(ListenSock).
270270

0 commit comments

Comments
 (0)