defmodule Nex.Handler.Errors do @moduledoc false import Plug.Conn def send_json_error(conn, status, message) do conn |> put_resp_content_type("application/json") |> send_resp(status, Jason.encode!(%{error: message})) end def send_error_page(conn, status, message, error) do is_htmx = get_req_header(conn, "hx-request") != [] is_json = match?(["api" | _], conn.path_info) or get_req_header(conn, "accept") |> Enum.any?(&String.contains?(&1, "application/json")) cond do is_json -> send_json_error(conn, status, message) is_htmx -> html = """
Error #{status}: #{html_escape(message)}
""" conn |> put_resp_content_type("text/html") |> send_resp(status, html) true -> html = build_error_page(conn, status, message, error) conn |> put_resp_content_type("text/html") |> send_resp(status, html) end end defp build_error_page(conn, status, message, error) do case resolve_convention_error_module(status) do {:ok, module} -> try do render_convention_error(module, status, message) rescue _ -> fallback_error_page(conn, status, message, error) end :none -> fallback_error_page(conn, status, message, error) end end defp resolve_convention_error_module(status) do app_module = Nex.Config.app_module() module_name = "#{app_module}.Pages.Error#{status}" case Nex.Utils.safe_to_existing_module(module_name) do {:ok, module} -> {:ok, module} :error -> :none end end defp render_convention_error(module, status, message) do assigns = %{ status: status, message: message, title: "#{status} — #{message}" } if function_exported?(module, :render, 1) do assigns |> module.render() |> to_html_binary() |> wrap_with_error_shell(module, assigns) else raise "Convention error module missing render/1" end end defp wrap_with_error_shell(content_html, module, assigns) do content_html |> wrap_with_app(module, assigns) |> wrap_with_document(assigns) end defp wrap_with_app(content_html, error_module, assigns) do case resolve_app_module(error_module) do nil -> content_html layout_module -> layout_assigns = assigns |> Map.put(:inner_content, content_html) |> Map.put_new(:title, "Nex App") layout_module.render(layout_assigns) |> to_html_binary() end end defp wrap_with_document(inner_html, assigns) do case resolve_document_module() do nil -> default_document(inner_html, Map.get(assigns, :title, "Nex App")) document_module -> document_assigns = %{ inner_content: inner_html, title: Map.get(assigns, :title, "Nex App") } document_module.render(document_assigns) |> to_html_binary() end end defp resolve_app_module(error_module) do case layout_override(error_module) do :none -> nil nil -> resolve_default_app_module() layout_module -> layout_module end end defp layout_override(error_module) do if function_exported?(error_module, :layout, 0) do case error_module.layout() do :none -> :none module when is_atom(module) -> module _ -> nil end else nil end end defp resolve_default_app_module do app_module = Nex.Config.app_module() with :error <- Nex.Utils.safe_to_existing_module("#{app_module}.Pages.App"), :error <- Nex.Utils.safe_to_existing_module("#{app_module}.Layouts") do nil else {:ok, module} -> module end end defp resolve_document_module do app_module = Nex.Config.app_module() case Nex.Utils.safe_to_existing_module("#{app_module}.Pages.Document") do {:ok, module} -> module :error -> nil end end defp default_document(inner_html, title) do """ #{title} #{inner_html} """ end defp to_html_binary(html) when is_binary(html), do: html defp to_html_binary(html), do: Phoenix.HTML.Safe.to_iodata(html) |> IO.iodata_to_binary() defp fallback_error_page(conn, status, message, error) do case Application.get_env(:nex_core, :error_page_module) do nil -> build_default_error_page(conn, status, message, error) module -> try do apply(module, :render_error, [conn, status, message, error]) rescue _ -> build_default_error_page(conn, status, message, error) end end end defp build_default_error_page(conn, status, message, error) do is_dev = dev_env?() {exception_section, stacktrace_section, request_section} = if is_dev && error != nil do {build_exception_section(error), build_stacktrace_section(), build_request_section(conn)} else {"", "", ""} end """ #{status} — #{html_escape(message)}
#{status}
#{html_escape(message)}
#{if is_dev, do: "Development mode — full error details below", else: "An unexpected error occurred"}
#{exception_section} #{stacktrace_section} #{request_section} ← Back to Home
""" end defp build_exception_section(error) do {type, msg} = case error do %{__struct__: mod, message: m} -> {inspect(mod), m} %{__struct__: mod} -> {inspect(mod), inspect(error)} _ -> {"RuntimeError", inspect(error, pretty: true)} end """
Exception
#{html_escape(type)}
    #{html_escape(msg)}
""" end defp build_stacktrace_section do stacktrace = Process.get(:nex_last_stacktrace, []) if stacktrace == [] do "" else frames = stacktrace |> Enum.take(15) |> Enum.map_join("", fn {mod, fun, arity, info} -> file = Keyword.get(info, :file, "unknown") |> to_string() line = Keyword.get(info, :line, 0) mod_str = inspect(mod) fun_str = "#{fun}/#{arity}" is_app = not String.starts_with?(mod_str, "Elixir.Phoenix") and not String.starts_with?(mod_str, "Elixir.Plug") and not String.starts_with?(mod_str, "Elixir.Bandit") and not String.starts_with?(mod_str, "Elixir.Nex.") frame_class = if is_app, do: "frame frame-app", else: "frame" """
#{html_escape(file)} :#{line} #{html_escape(mod_str)}.#{html_escape(fun_str)}
""" entry -> "
#{html_escape(inspect(entry))}
" end) """
Stacktrace (app frames highlighted)
#{frames}
""" end end defp build_request_section(conn) do params_str = case conn.params do %Plug.Conn.Unfetched{} -> "(unfetched)" params -> inspect(params, pretty: true, limit: 10) end rows = [ {"Method", conn.method}, {"Path", conn.request_path}, {"Params", params_str}, {"Host", conn.host} ] rows_html = Enum.map_join(rows, "", fn {key, value} -> """
#{key} #{html_escape(to_string(value))}
""" end) """
Request
#{rows_html}
""" end defp dev_env?, do: Nex.Config.dev?() defp html_escape(text) when is_binary(text) do Phoenix.HTML.html_escape(text) |> Phoenix.HTML.safe_to_string() end defp html_escape(text) do text |> to_string() |> html_escape() end end