diff --git a/doc/src/guide/transports.asciidoc b/doc/src/guide/transports.asciidoc index 73747fd587..bdccfc91df 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 c26d91c509..4717281783 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 f0cff68364..26c5972ccb 100644 --- a/src/ranch_acceptors_sup.erl +++ b/src/ranch_acceptors_sup.erl @@ -89,8 +89,8 @@ listen_error(Ref, Transport, TransOpts0, Reason, Logger) -> SocketOpts = hide_socket_opts(SocketOpts0), 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), + "Failed to start Ranch listener ~p in ~p:listen(~0p) for reason ~p (~s)~n", + [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 an +%% 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("~0p", [Reason])) + end. diff --git a/src/ranch_conns_sup.erl b/src/ranch_conns_sup.erl index 05ef84c414..f142f4104a 100644 --- a/src/ranch_conns_sup.erl +++ b/src/ranch_conns_sup.erl @@ -138,7 +138,7 @@ loop(State=#state{parent=Parent, ref=Ref, id=Id, conn_type=ConnType, To ! self(), ranch:log(error, "Ranch listener ~p connection process start failure; " - "~p:start_link/3 returned: ~999999p~n", + "~p:start_link/3 returned: ~0p~n", [Ref, Protocol, Ret], Logger), Transport:close(Socket), loop(State, CurConns, NbChildren, Sleepers) @@ -146,7 +146,7 @@ loop(State=#state{parent=Parent, ref=Ref, id=Id, conn_type=ConnType, To ! self(), ranch:log(error, "Ranch listener ~p connection process start failure; " - "~p:start_link/3 crashed with reason: ~p:~999999p~n", + "~p:start_link/3 crashed with reason: ~p:~0p~n", [Ref, Protocol, Class, Reason], Logger), Transport:close(Socket), loop(State, CurConns, NbChildren, Sleepers) @@ -475,7 +475,7 @@ system_terminate(Reason, _, _, {State, _, NbChildren, _}) -> system_code_change(Misc, _, _, _) -> {ok, Misc}. -%% We use ~999999p here instead of ~w because the latter doesn't +%% We use ~0p here instead of ~w because the latter doesn't %% support printable strings. report_error(_, _, _, _, normal) -> ok; @@ -486,5 +486,5 @@ report_error(_, _, _, _, {shutdown, _}) -> report_error(Logger, Ref, Protocol, Pid, Reason) -> ranch:log(error, "Ranch listener ~p had connection process started with " - "~p:start_link/3 at ~p exit with reason: ~999999p~n", + "~p:start_link/3 at ~p exit with reason: ~0p~n", [Ref, Protocol, Pid, Reason], Logger). diff --git a/src/ranch_ssl.erl b/src/ranch_ssl.erl index 7e6b9d8665..d2cf749519 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 354128905b..70e9f460d8 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 52eeba3fd6..2fbcc1aa79 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 ba57c86f3e..e5df13433b 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(""), + 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 0000000000..51cb64e53c --- /dev/null +++ b/test/ranch_listen_error_transport.erl @@ -0,0 +1,194 @@ +%% Copyright (c) 2024, Jan Uhlig +%% +%% 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).