# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.
use Croma
defmodule AntikytheraCore.Handler.GearError do
alias Antikythera.{Conn, Request, Http.Method, ErrorReason, GearName}
alias Antikythera.ExecutorPool.BadIdReason
alias AntikytheraCore.Conn, as: CoreConn
alias AntikytheraCore.GearModule
@typep reason :: ErrorReason.gear_action_error_reason()
@typep stacktrace :: ErrorReason.stacktrace()
defun error(conn :: v[Conn.t()], reason :: reason, stacktrace :: stacktrace) :: Conn.t() do
output_error_to_log(conn, reason, stacktrace)
invoke_error_handler(
conn,
fn mod -> mod.error(conn, reason) end,
fn ->
%Conn{conn | status: 500, resp_body: internal_error_body(conn, reason, stacktrace)}
end
)
end
defun no_route(conn :: v[Conn.t()]) :: Conn.t() do
invoke_error_handler(
conn,
fn mod -> mod.no_route(conn) end,
fn -> %Conn{conn | status: 400, resp_body: "NoRouteFound"} end
)
end
defun bad_request(conn :: v[Conn.t()]) :: Conn.t() do
invoke_error_handler(
conn,
fn mod -> mod.bad_request(conn) end,
fn -> %Conn{conn | status: 400, resp_body: "BadRequest"} end
)
end
defun bad_executor_pool_id(conn :: v[Conn.t()], reason :: v[BadIdReason.t()]) :: Conn.t() do
invoke_error_handler(
conn,
fn mod -> mod.bad_executor_pool_id(conn, reason) end,
fn -> %Conn{conn | status: 404, resp_body: "InvalidExecutorPoolId"} end
)
end
defun ws_too_many_connections(conn :: v[Conn.t()]) :: Conn.t() do
invoke_error_handler(
conn,
fn mod -> mod.ws_too_many_connections(conn) end,
fn -> %Conn{conn | status: 503, resp_body: "TooManyWebsocketConnections"} end
)
end
defunp invoke_error_handler(
conn :: v[Conn.t()],
invoke_fn :: (module -> Conn.t()),
default_fn :: (() -> Conn.t())
) :: Conn.t() do
case CoreConn.gear_name(conn) |> GearModule.error_handler() do
nil ->
default_fn.()
mod ->
try do
invoke_fn.(mod)
rescue
_ ->
# The raised exception can be an `UndefinedFunctionError` in case of optional handler functions;
# otherwise it can be an arbitrary exception due to bugs in handler implementations.
# Anyway we fallback to `default_fn`.
default_fn.()
end
end
end
defunp output_error_to_log(
%Conn{request: request} = conn,
reason :: reason,
stacktrace :: stacktrace
) :: :ok do
%Request{method: method, path_info: path_info} = request
path = "/" <> Enum.join(path_info, "/")
gear_name = CoreConn.gear_name(conn)
log_message = "#{Method.to_string(method)} #{path} #{ErrorReason.format(reason, stacktrace)}"
do_output_error_to_log(gear_name, log_message, reason)
end
if Antikythera.Env.compile_env() == :undefined do
alias AntikytheraCore.GearLog.Writer, warn: false
defunp do_output_error_to_log(
gear_name :: v[GearName.t()],
log_message :: v[String.t()],
reason :: reason
) :: :ok do
gear_logger_module = gear_name |> GearModule.logger()
case reason do
{:error, %ExUnit.AssertionError{}} ->
:ok = Writer.set_write_to_terminal(gear_name, true)
gear_logger_module.error(log_message)
:ok = Writer.restore_write_to_terminal(gear_name)
_ ->
gear_logger_module.error(log_message)
end
end
else
defunp do_output_error_to_log(
gear_name :: v[GearName.t()],
log_message :: v[String.t()],
_reason :: reason
) :: :ok do
gear_logger_module = gear_name |> GearModule.logger()
gear_logger_module.error(log_message)
end
end
if Application.compile_env!(:antikythera, :return_detailed_info_on_error?) do
defun internal_error_body(conn :: Conn.t(), reason :: reason, stacktrace :: stacktrace) ::
String.t() do
[
"InternalError",
"",
"Error reason: #{inspect(reason, structs: false)}",
"",
"Conn:",
inspect(conn, pretty: true, structs: false),
"",
"Stacktrace (list of {module, function, arity, location}):",
"[",
Enum.map_join(stacktrace, "\n", fn s -> " #{inspect(s, structs: false)}" end),
"]"
]
|> Enum.join("\n")
end
else
defun internal_error_body(_conn :: Conn.t(), _reason :: reason, _stacktrace :: stacktrace) ::
String.t() do
"InternalError"
end
end
end