defmodule Hibana.Plugins.DevErrorPage do @moduledoc """ Rich error pages for development with stack traces, request info, and code context. ## Usage # Only in dev! plug Hibana.Plugins.DevErrorPage ## Options - `:enabled` - Whether the dev error page rendering is active (default: `true`) """ use Hibana.Plugin import Plug.Conn @impl true def init(opts), do: %{enabled: Keyword.get(opts, :enabled, true)} @impl true def call(conn, %{enabled: true}) do register_before_send(conn, fn conn -> if conn.status >= 500 and not conn.halted do render_error_page(conn) else conn end end) end def call(conn, _), do: conn @doc "Render an exception as a rich HTML error page" def render_exception(conn, kind, reason, stacktrace) do html = build_error_html(kind, reason, stacktrace, conn) conn |> put_resp_content_type("text/html") |> send_resp(500, html) |> halt() end defp render_error_page(conn) do %{conn | resp_body: build_simple_error_html(conn)} end defp build_error_html(kind, reason, stacktrace, conn) do title = exception_title(kind, reason) message = Exception.message(reason) |> html_escape() stack_html = format_stacktrace(stacktrace) request_html = format_request(conn) """ #{title}

#{title}

#{message}

Request

#{request_html}

Stack Trace

#{stack_html}
Hibana Dev Error Page -- This page is only shown in development.
""" end defp build_simple_error_html(conn) do """ 500 Error

500 -- Internal Server Error

#{conn.method} #{conn.request_path}\nStatus: #{conn.status}
""" end defp exception_title(:error, %{__struct__: mod}), do: inspect(mod) defp exception_title(kind, _), do: to_string(kind) defp format_stacktrace(stacktrace) do stacktrace |> Enum.map(fn {mod, fun, arity, location} -> file = Keyword.get(location, :file, ~c"?") |> to_string() line = Keyword.get(location, :line, 0) arity_str = if is_list(arity), do: length(arity), else: arity is_app = not String.contains?(file, "/deps/") and not String.starts_with?(file, "(") class = if is_app, do: "app", else: "lib" """
#{inspect(mod)}.#{fun}/#{arity_str} #{file}:#{line}
""" _ -> "" end) |> Enum.join("\n") end defp format_request(conn) do headers = conn.req_headers |> Enum.map(fn {k, v} -> "#{k}#{html_escape(v)}" end) |> Enum.join() """ #{headers}
Method#{conn.method}
Path#{conn.request_path}
Query#{conn.query_string}
Remote IP#{conn.remote_ip |> Tuple.to_list() |> Enum.join(".")}
""" end defp html_escape(str) when is_binary(str) do str |> String.replace("&", "&") |> String.replace("<", "<") |> String.replace(">", ">") |> String.replace("\"", """) end defp html_escape(other), do: html_escape(to_string(other)) end