Skip to content

Commit

Permalink
Add mochiweb_request:is_closed/1 function
Browse files Browse the repository at this point in the history
This function can used during long running request callbacks to detect if the
client connection is closed.

If the request callback periodically streams data back to the client, the act
of writting to the client socket will detect if it is closed or not. However,
in cases when no data is sent back, and the client times-out and closes the
connection, it may be useful to be able to find out early and stop processing
the request on the server.

It turns out there is no easy way to detect if a passive mode socket is closed
in Erlang/OTP [1]. Neither one of inet:monitor/1, inet:info/1, inet:getstat/1
work. However, it is possible to do it by querying the TCP state info of the
socket. That option available in Linux since kernel 2.4 and on other Unix-like
OSes (NetBSD, OpenBSD, FreeBSD and MacOS). Windows also has a tcp info query
method however it is not reacheable via the gensockopts(2) standard socket API,
so it can't be queried from Erlang's inet:getopts/2 API.

[1] Using the newer socket module it's possible to detect if a socket is closed
by attempting a recv with a MSG_PEEK option. However, the regular gen_tcp OTP
module doesn't have a recv() variant which takes extra options. In addition,
the new socket implementation still feels rather experimental. (It's not the
default even in the latest OTP 26 release).
  • Loading branch information
nickva committed Aug 31, 2023
1 parent 0733494 commit 7d2a79d
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/mochiweb_request.erl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
-export([accepted_content_types/2,
accepts_content_type/2]).

-export([is_closed/1]).

-define(SAVE_QS, mochiweb_request_qs).

-define(SAVE_PATH, mochiweb_request_path).
Expand Down Expand Up @@ -1140,3 +1142,13 @@ accept_header({?MODULE,
undefined -> "*/*";
Value -> Value
end.

%% @spec is_closed(request()) -> true | false | undefined
%% @doc Check if a request connection is closing or already closed. This may be
%% useful when processing long running request callbacks, when the client
%% disconnects after a short timeout. This function works on Linux, NetBSD,
%% OpenBSD, FreeBSD and MacOS. On other operating systems, like Windows for
%% instance, it will return undefined.
is_closed({?MODULE,
[Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
mochiweb_socket:is_closed(Socket).
110 changes: 109 additions & 1 deletion src/mochiweb_socket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
-export([listen/4,
accept/1, transport_accept/1, finish_accept/1,
recv/3, send/2, close/1, port/1, peername/1,
setopts/2, getopts/2, type/1, exit_if_closed/1]).
setopts/2, getopts/2, type/1, exit_if_closed/1,
is_closed/1]).

-define(ACCEPT_TIMEOUT, 2000).
-define(SSL_TIMEOUT, 10000).
Expand Down Expand Up @@ -180,3 +181,110 @@ exit_if_closed({error, einval = Error}) ->
exit({shutdown, Error});
exit_if_closed(Res) ->
Res.

%% @doc Check if the socket is closing or already closed. This function works
%% with passive mode sockets on Linux, OpenBSD, NetBSD, FreeBSD and MacOS. On
%% unsupported OS-es, like Windows, it returns undefined.
is_closed(Socket) ->
OsType = os:type(),
case tcp_info_opt(OsType) of
{raw, _, _, _} = InfoOpt ->
case getopts(Socket, [InfoOpt]) of
{ok, [{raw, _, _, <<State:8/native, _/binary>>}]} ->
tcp_is_closed(State, OsType);
{ok, []} ->
undefined;
{error, einval} ->
% Already cleaned up
true;
{error, _} ->
undefined
end;
undefined ->
undefined
end.

% All OS-es have the tcpi_state (uint8) as first member of tcp_info struct

tcp_info_opt({unix, linux}) ->
%% netinet/in.h
%% IPPROTO_TCP = 6
%%
%% netinet/tcp.h
%% #define TCP_INFO 11
%%
{raw, 6, 11, 1};
tcp_info_opt({unix, darwin}) ->
%% netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% netinet/tcp.h
%% #define TCP_CONNECTION_INFO 0x106
%%
{raw, 6, 16#106, 1};
tcp_info_opt({unix, freebsd}) ->
%% sys/netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% sys/netinet/tcp.h
%% #define TCP_INFO 32
%%
{raw, 6, 32, 1};
tcp_info_opt({unix, netbsd}) ->
%% sys/netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% sys/netinet/tcp.h
%% #define TCP_INFO 9
{raw, 6, 9, 1};
tcp_info_opt({unix, openbsd}) ->
%% sys/netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% sys/netinet/tcp.h
%% #define TCP_INFO 0x09
{raw, 6, 16#09, 1};
tcp_info_opt({_, _}) ->
undefined.

tcp_is_closed(State, {unix, linux}) ->
%% netinet/tcp.h
%% enum
%% {
%% TCP_ESTABLISHED = 1,
%% TCP_SYN_SENT,
%% TCP_SYN_RECV,
%% TCP_FIN_WAIT1,
%% TCP_FIN_WAIT2,
%% TCP_TIME_WAIT,
%% TCP_CLOSE,
%% TCP_CLOSE_WAIT,
%% TCP_LAST_ACK,
%% TCP_LISTEN,
%% TCP_CLOSING
%% }
%%
lists:member(State, [4, 5, 6, 7, 8, 9, 11]);
tcp_is_closed(State, {unix, Type})
when
Type =:= darwin;
Type =:= freebsd;
Type =:= netbsd;
Type =:= openbsd
->
%% tcp_fsm.h states are the same on macos, freebsd, netbsd and openbsd
%%
%% netinet/tcp_fsm.h
%% #define TCPS_CLOSED 0 /* closed */
%% #define TCPS_LISTEN 1 /* listening for connection */
%% #define TCPS_SYN_SENT 2 /* active, have sent syn */
%% #define TCPS_SYN_RECEIVED 3 /* have send and received syn */
%% #define TCPS_ESTABLISHED 4 /* established */
%% #define TCPS_CLOSE_WAIT 5 /* rcvd fin, waiting for close */
%% #define TCPS_FIN_WAIT_1 6 /* have closed, sent fin */
%% #define TCPS_CLOSING 7 /* closed xchd FIN; await FIN ACK */
%% #define TCPS_LAST_ACK 8 /* had fin and close; await FIN ACK */
%% #define TCPS_FIN_WAIT_2 9 /* have closed, fin is acked */
%% #define TCPS_TIME_WAIT 10 /* in 2*msl quiet wait after close */
%%
lists:member(State, [0, 5, 6, 7, 8, 9, 10]).
23 changes: 23 additions & 0 deletions test/mochiweb_request_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,27 @@ should_close_test() ->
(F({1, 0}, [{"Connection", "Keep-Alive"}]))),
ok.

is_closed_test() ->
Headers = mochiweb_headers:make([{"Accept", "text/html"}]),
{ok, Socket} = gen_tcp:listen(0, [{active, false}]),
Req = mochiweb_request:new(Socket, 'GET', "/foo", {1, 1}, Headers),
case is_closed_supported() of
true ->
?assertNot(mochiweb_request:is_closed(Req)),
gen_tcp:close(Socket),
?assert(mochiweb_request:is_closed(Req));
false ->
?assertEqual(undefined, mochiweb_request:is_closed(Req)),
gen_tcp:close(Socket),
?assertEqual(undefined, mochiweb_request:is_closed(Req))
end.

is_closed_supported() ->
case os:type() of
{unix, OsName} ->
lists:member(OsName, [linux, openbsd, netbsd, freebsd, darwin]);
{_, _} ->
false
end.

-endif.

0 comments on commit 7d2a79d

Please sign in to comment.