aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Uhlig <[email protected]>2024-11-21 10:43:22 +0100
committerLoïc Hoguin <[email protected]>2025-01-23 12:36:43 +0100
commit38100182914ca3ec84afbdf32ca84b8b78c40973 (patch)
tree424e2087b5df44f8080fdf1504eb00a4af51d354
parent2a2ff882d128bc58ac99912f86ccf8e57d47ed1f (diff)
downloadranch-38100182914ca3ec84afbdf32ca84b8b78c40973.tar.gz
ranch-38100182914ca3ec84afbdf32ca84b8b78c40973.tar.bz2
ranch-38100182914ca3ec84afbdf32ca84b8b78c40973.zip
Add optional callback ranch_transport:format_error/1
-rw-r--r--doc/src/guide/transports.asciidoc5
-rw-r--r--doc/src/manual/ranch_transport.asciidoc13
-rw-r--r--src/ranch_acceptors_sup.erl20
-rw-r--r--src/ranch_ssl.erl7
-rw-r--r--src/ranch_tcp.erl5
-rw-r--r--src/ranch_transport.erl4
-rw-r--r--test/acceptor_SUITE.erl24
-rw-r--r--test/ranch_listen_error_transport.erl194
8 files changed, 266 insertions, 6 deletions
diff --git a/doc/src/guide/transports.asciidoc b/doc/src/guide/transports.asciidoc
index 73747fd..bdccfc9 100644
--- a/doc/src/guide/transports.asciidoc
+++ b/doc/src/guide/transports.asciidoc
@@ -175,3 +175,8 @@ implement the `{active, once}` and the `{active, true}` options.
If the transport handler doesn't have a native implementation of `sendfile/5` a
fallback is available, `ranch_transport:sendfile/6`. The extra first argument
is the transport's module. See `ranch_ssl` for an example.
+
+It is highly recommended for a custom transport handler to implement the
+optional `format_error/1` callback, in order to provide a human-readable
+diagnostic string. Implementing this callback will become mandatory in
+Ranch 3.0.
diff --git a/doc/src/manual/ranch_transport.asciidoc b/doc/src/manual/ranch_transport.asciidoc
index c26d91c..4717281 100644
--- a/doc/src/manual/ranch_transport.asciidoc
+++ b/doc/src/manual/ranch_transport.asciidoc
@@ -47,6 +47,19 @@ Assign a new controlling process to the socket. The
controlling process is the process that is linked to
and receives messages from the socket.
+=== format_error
+
+[source,erlang]
+----
+format_error(Reason :: term())
+ -> ReasonString :: string()
+----
+
+Format a listen error into a human-readable diagnostic string.
+
+This callback is optional, but implementing it is highly
+recommended. It will become mandatory in Ranch 3.0.
+
=== getopts
[source,erlang]
diff --git a/src/ranch_acceptors_sup.erl b/src/ranch_acceptors_sup.erl
index f0cff68..a4afc18 100644
--- a/src/ranch_acceptors_sup.erl
+++ b/src/ranch_acceptors_sup.erl
@@ -90,7 +90,7 @@ listen_error(Ref, Transport, TransOpts0, Reason, Logger) ->
TransOpts = TransOpts0#{socket_opts => SocketOpts},
ranch:log(error,
"Failed to start Ranch listener ~p in ~p:listen(~999999p) for reason ~p (~s)~n",
- [Ref, Transport, TransOpts, Reason, format_error(Reason)], Logger),
+ [Ref, Transport, TransOpts, Reason, format_error(Transport, Reason)], Logger),
exit({listen_error, Ref, Reason}).
hide_socket_opts([]) ->
@@ -106,9 +106,19 @@ hide_socket_opts([{password, _}|SocketOpts]) ->
hide_socket_opts([SocketOpt|SocketOpts]) ->
[SocketOpt|hide_socket_opts(SocketOpts)].
-format_error(no_cert) ->
+%% Handling of no_cert really should be done in ranch_ssl. We leave it here for
+%% backwards compatibility with possibly existing custom transports without a
+%% format_error/1 implementation that may rely on this module handling it.
+%% TODO: Remove in Ranch 3.0
+format_error(_, no_cert) ->
"no certificate provided; see cert, certfile, sni_fun or sni_hosts options";
-format_error(reuseport_local) ->
+format_error(_, reuseport_local) ->
"num_listen_sockets must be set to 1 for local sockets";
-format_error(Reason) ->
- inet:format_error(Reason).
+format_error(Transport, Reason) ->
+ %% TODO: Required callback in Ranch 3.0
+ case erlang:function_exported(Transport, format_error, 1) of
+ true ->
+ Transport:format_error(Reason);
+ false ->
+ lists:flatten(io_lib:format("~999999p", [Reason]))
+ end.
diff --git a/src/ranch_ssl.erl b/src/ranch_ssl.erl
index 7e6b9d8..c99335f 100644
--- a/src/ranch_ssl.erl
+++ b/src/ranch_ssl.erl
@@ -46,6 +46,7 @@
-export([shutdown/2]).
-export([close/1]).
-export([cleanup/1]).
+-export([format_error/1]).
-type ssl_opt() :: {alpn_preferred_protocols, [binary()]}
| {anti_replay, '10k' | '100k' | {integer(), integer(), integer()}}
@@ -328,6 +329,12 @@ cleanup(#{socket_opts:=SocketOpts}) ->
cleanup(_) ->
ok.
+-spec format_error({error, ssl:reason()} | ssl:reason()) -> string().
+format_error(no_cert) ->
+ "no certificate provided; see cert, certfile, sni_fun or sni_hosts options";
+format_error(Reason) ->
+ ssl:format_error(Reason).
+
get_tls_versions(SocketOpts) ->
%% Socket options need to be reversed for keyfind because later options
%% take precedence when contained multiple times, but keyfind will return
diff --git a/src/ranch_tcp.erl b/src/ranch_tcp.erl
index 3541289..70e9f46 100644
--- a/src/ranch_tcp.erl
+++ b/src/ranch_tcp.erl
@@ -45,6 +45,7 @@
-export([shutdown/2]).
-export([close/1]).
-export([cleanup/1]).
+-export([format_error/1]).
-type opt() :: {backlog, non_neg_integer()}
| {buffer, non_neg_integer()}
@@ -285,3 +286,7 @@ cleanup(#{socket_opts:=SocketOpts}) ->
end;
cleanup(_) ->
ok.
+
+-spec format_error(inet:posix() | system_limit) -> string().
+format_error(Reason) ->
+ inet:format_error(Reason).
diff --git a/src/ranch_transport.erl b/src/ranch_transport.erl
index 52eeba3..2fbcc1a 100644
--- a/src/ranch_transport.erl
+++ b/src/ranch_transport.erl
@@ -68,6 +68,10 @@
-> ok | {error, atom()}.
-callback close(socket()) -> ok.
-callback cleanup(ranch:transport_opts(any())) -> ok.
+-callback format_error(term()) -> string().
+
+%% TODO: Required callback in Ranch 3.0
+-optional_callbacks([format_error/1]).
%% A fallback for transports that don't have a native sendfile implementation.
%% Note that the ordering of arguments is different from file:sendfile/5 and
diff --git a/test/acceptor_SUITE.erl b/test/acceptor_SUITE.erl
index ba57c86..0f96fae 100644
--- a/test/acceptor_SUITE.erl
+++ b/test/acceptor_SUITE.erl
@@ -114,7 +114,8 @@ groups() ->
misc_multiple_ip_local_socket_opts,
misc_connection_alarms,
misc_stop_unknown_listener,
- misc_repeated_start_stop
+ misc_repeated_start_stop,
+ misc_listen_error
]}, {supervisor, [
connection_type_supervisor,
connection_type_supervisor_separate_from_connection,
@@ -683,6 +684,27 @@ misc_repeated_start_stop(_) ->
),
ok.
+misc_listen_error(_) ->
+ doc("Ensure Transport:format_error/1 gets called on Transport:listen/1 error."),
+ Name = name(),
+ Self = self(),
+ logger:add_primary_filter(Name, {fun(Event, _Args) -> Self ! {Name, Event}, ignore end, undefined}),
+ {error, _} = ranch:start_listener(Name,
+ ranch_listen_error_transport, #{},
+ echo_protocol, []),
+ receive
+ {Name, #{msg := {Format, Args}}} ->
+ FormattedMsg = lists:flatten(io_lib:format(Format, Args)),
+ NormalizedMsg = re:replace(FormattedMsg, "\\s", "", [global, {return, list}]),
+ "FailedtostartRanchlistenermisc_listen_error"
+ "inranch_listen_error_transport:listen(#{socket_opts=>[]})"
+ "forreason{ranch_listen_error_transport,listen_error}"
+ "(Therewasanerrorinranch_listen_error_transport:listen_error)" = NormalizedMsg
+ after 1000 ->
+ error(timeout)
+ end,
+ logger:remove_primary_filter(Name),
+ ok.
%% ssl.
diff --git a/test/ranch_listen_error_transport.erl b/test/ranch_listen_error_transport.erl
new file mode 100644
index 0000000..51cb64e
--- /dev/null
+++ b/test/ranch_listen_error_transport.erl
@@ -0,0 +1,194 @@
+%% Copyright (c) 2024, Jan Uhlig <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(ranch_listen_error_transport).
+-behaviour(ranch_transport).
+
+-export([name/0]).
+-export([secure/0]).
+-export([messages/0]).
+-export([listen/1]).
+-export([disallowed_listen_options/0]).
+-export([accept/2]).
+-export([handshake/2]).
+-export([handshake/3]).
+-export([handshake_continue/2]).
+-export([handshake_continue/3]).
+-export([handshake_cancel/1]).
+-export([connect/3]).
+-export([connect/4]).
+-export([recv/3]).
+-export([recv_proxy_header/2]).
+-export([send/2]).
+-export([sendfile/2]).
+-export([sendfile/4]).
+-export([sendfile/5]).
+-export([setopts/2]).
+-export([getopts/2]).
+-export([getstat/1]).
+-export([getstat/2]).
+-export([controlling_process/2]).
+-export([peername/1]).
+-export([sockname/1]).
+-export([shutdown/2]).
+-export([close/1]).
+-export([cleanup/1]).
+-export([format_error/1]).
+
+-type opt() :: ranch_tcp:opt().
+-export_type([opt/0]).
+
+-type opts() :: ranch_tcp:opts().
+-export_type([opts/0]).
+
+-spec name() -> tcp.
+name() -> tcp.
+
+-spec secure() -> boolean().
+secure() ->
+ false.
+
+-spec messages() -> {tcp, tcp_closed, tcp_error, tcp_passive}.
+messages() -> {tcp, tcp_closed, tcp_error, tcp_passive}.
+
+-spec listen(ranch:transport_opts(opts())) -> {ok, inet:socket()} | {error, atom()}.
+listen(_TransOpts) ->
+ {error, {?MODULE, listen_error}}.
+
+-spec disallowed_listen_options() -> [atom()].
+disallowed_listen_options() ->
+ ranch_tcp:disallowed_listen_options().
+
+-spec accept(inet:socket(), timeout())
+ -> {ok, inet:socket()} | {error, closed | timeout | atom()}.
+accept(LSocket, Timeout) ->
+ ranch_tcp:accept(LSocket, Timeout).
+
+-spec handshake(inet:socket(), timeout()) -> {ok, inet:socket()}.
+handshake(CSocket, Timeout) ->
+ iranch_tcp:handshake(CSocket, Timeout).
+
+-spec handshake(inet:socket(), opts(), timeout()) -> {ok, inet:socket()}.
+handshake(CSocket, Opts, Timeout) ->
+ ranch_tcp:handshake(CSocket, Opts, Timeout).
+
+-spec handshake_continue(inet:socket(), timeout()) -> no_return().
+handshake_continue(CSocket, Timeout) ->
+ ranch_tcp:handshake_continue(CSocket, Timeout).
+
+-spec handshake_continue(inet:socket(), opts(), timeout()) -> no_return().
+handshake_continue(CSocket, Opts, Timeout) ->
+ ranch_tcp:handshake_continue(CSocket, Opts, Timeout).
+
+-spec handshake_cancel(inet:socket()) -> no_return().
+handshake_cancel(CSocket) ->
+ ranch_tcp:handshake_cancel(CSocket).
+
+%% @todo Probably filter Opts?
+-spec connect(inet:ip_address() | inet:hostname(),
+ inet:port_number(), any())
+ -> {ok, inet:socket()} | {error, atom()}.
+connect(Host, Port, Opts) when is_integer(Port) ->
+ ranch_tcp:connect(Host, Port, Opts).
+
+%% @todo Probably filter Opts?
+-spec connect(inet:ip_address() | inet:hostname(),
+ inet:port_number(), any(), timeout())
+ -> {ok, inet:socket()} | {error, atom()}.
+connect(Host, Port, Opts, Timeout) when is_integer(Port) ->
+ ranch_tcp:connect(Host, Port, Opts, Timeout).
+
+-spec recv(inet:socket(), non_neg_integer(), timeout())
+ -> {ok, any()} | {error, closed | atom()}.
+recv(Socket, Length, Timeout) ->
+ ranch_tcp:recv(Socket, Length, Timeout).
+
+-spec recv_proxy_header(inet:socket(), timeout())
+ -> {ok, ranch_proxy_header:proxy_info()}
+ | {error, closed | atom()}
+ | {error, protocol_error, atom()}.
+recv_proxy_header(Socket, Timeout) ->
+ ranch_tcp:recv_proxy_header(Socket, Timeout).
+
+-spec send(inet:socket(), iodata()) -> ok | {error, atom()}.
+send(Socket, Packet) ->
+ ranch_tcp:send(Socket, Packet).
+
+-spec sendfile(inet:socket(), file:name_all() | file:fd())
+ -> {ok, non_neg_integer()} | {error, atom()}.
+sendfile(Socket, Filename) ->
+ ranch_tcp:sendfile(Socket, Filename).
+
+-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(),
+ non_neg_integer())
+ -> {ok, non_neg_integer()} | {error, atom()}.
+sendfile(Socket, File, Offset, Bytes) ->
+ ranch_tcp:sendfile(Socket, File, Offset, Bytes).
+
+-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(),
+ non_neg_integer(), [{chunk_size, non_neg_integer()}])
+ -> {ok, non_neg_integer()} | {error, atom()}.
+sendfile(Socket, Filename, Offset, Bytes, Opts) ->
+ ranch_tcp:sendfile(Socket, Filename, Offset, Bytes, Opts).
+
+%% @todo Probably filter Opts?
+-spec setopts(inet:socket(), list()) -> ok | {error, atom()}.
+setopts(Socket, Opts) ->
+ ranch_tcp:setopts(Socket, Opts).
+
+-spec getopts(inet:socket(), [atom()]) -> {ok, list()} | {error, atom()}.
+getopts(Socket, Opts) ->
+ ranch_tcp:getopts(Socket, Opts).
+
+-spec getstat(inet:socket()) -> {ok, list()} | {error, atom()}.
+getstat(Socket) ->
+ ranch_tcp:getstat(Socket).
+
+-spec getstat(inet:socket(), [atom()]) -> {ok, list()} | {error, atom()}.
+getstat(Socket, OptionNames) ->
+ ranch_tcp:getstat(Socket, OptionNames).
+
+-spec controlling_process(inet:socket(), pid())
+ -> ok | {error, closed | not_owner | atom()}.
+controlling_process(Socket, Pid) ->
+ ranch_tcp:controlling_process(Socket, Pid).
+
+-spec peername(inet:socket())
+ -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
+peername(Socket) ->
+ ranch_tcp:peername(Socket).
+
+-spec sockname(inet:socket())
+ -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
+sockname(Socket) ->
+ ranch_tcp:sockname(Socket).
+
+-spec shutdown(inet:socket(), read | write | read_write)
+ -> ok | {error, atom()}.
+shutdown(Socket, How) ->
+ ranch_tcp:shutdown(Socket, How).
+
+-spec close(inet:socket()) -> ok.
+close(Socket) ->
+ ranch_tcp:close(Socket).
+
+-spec cleanup(ranch:transport_opts(opts())) -> ok.
+cleanup(Opts) ->
+ ranch_tcp:cleanup(Opts).
+
+-spec format_error(inet:posix() | system_limit) -> string().
+format_error({?MODULE, Reason}) ->
+ io_lib:format("There was an error in ~0p: ~0p", [?MODULE, Reason]);
+format_error(Reason) ->
+ ranch_tcp:format_error(Reason).