# 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