Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional callback ranch_transport:format_error/1 #354

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/src/guide/transports.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 13 additions & 0 deletions doc/src/manual/ranch_transport.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
22 changes: 16 additions & 6 deletions src/ranch_acceptors_sup.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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([]) ->
Expand All @@ -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) ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually belongs in ranch_ssl:format_error/1 (and is replicated there). For backwards compatibility with existing custom transports without an format_error implementation which may rely on ranch_acceptors_sup handling this, I left it in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write that in a comment.

"no certificate provided; see cert, certfile, sni_fun or sni_hosts options";
format_error(reuseport_local) ->
format_error(_, reuseport_local) ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is generated by ranch_acceptors_sup itself as it is a misconfiguration specific to ranch. Therefore, I left it here, it doesn't seem to fit into a transport.

"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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment saying this will be required in 3.0, and for now let's consider/document it as optional.

true ->
Transport:format_error(Reason);
false ->
lists:flatten(io_lib:format("~0p", [Reason]))
end.
8 changes: 4 additions & 4 deletions src/ranch_conns_sup.erl
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,15 @@ 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)
catch Class:Reason ->
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)
Expand Down Expand Up @@ -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;
Expand All @@ -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).
7 changes: 7 additions & 0 deletions src/ranch_ssl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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()}}
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/ranch_tcp.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Expand Down Expand Up @@ -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).
4 changes: 4 additions & 0 deletions src/ranch_transport.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion test/acceptor_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down
194 changes: 194 additions & 0 deletions test/ranch_listen_error_transport.erl
Original file line number Diff line number Diff line change
@@ -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).
Loading