defmodule MicrowavepropWeb.Api.ErrorJSON do @moduledoc """ RFC 9457 problem+json error responses for `/api/v1`. All error bodies share the shape: { "type": "about:blank", "title": "", "status": , "detail": "", "errors": } """ import Plug.Conn alias Ecto.Changeset alias Plug.Conn.Status @content_type "application/problem+json" @doc "Halts the connection with a problem+json body." @spec send_problem(Plug.Conn.t(), pos_integer(), String.t(), String.t(), map()) :: Plug.Conn.t() def send_problem(conn, status, title, detail, extra \\ %{}) do body = %{ type: "about:blank", title: title, status: status, detail: detail } |> Map.merge(extra) |> Jason.encode!() conn |> put_resp_content_type(@content_type) |> send_resp(status, body) |> halt() end @doc "Halts with a 422 problem+json including a flattened changeset error map." @spec send_changeset(Plug.Conn.t(), Changeset.t()) :: Plug.Conn.t() def send_changeset(conn, %Changeset{} = changeset) do send_problem( conn, 422, "validation_failed", "One or more fields are invalid.", %{errors: translate_errors(changeset)} ) end @doc "Translates a changeset's errors into a flat map of field -> [messages]." @spec translate_errors(Changeset.t()) :: map() def translate_errors(%Changeset{} = changeset) do Changeset.traverse_errors(changeset, fn {msg, opts} -> Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", to_string(value)) end) end) end ## Default Phoenix error renderer hooks ---------------------------- @doc false @spec render(String.t(), map()) :: map() def render("400.json", _assigns), do: error_body(400, "bad_request", "Malformed request.") def render("401.json", _assigns), do: error_body(401, "unauthorized", "Authentication is required.") def render("403.json", _assigns), do: error_body(403, "forbidden", "You may not access this resource.") def render("404.json", _assigns), do: error_body(404, "not_found", "Resource not found.") def render("405.json", _assigns), do: error_body(405, "method_not_allowed", "HTTP method not allowed.") def render("415.json", _assigns), do: error_body(415, "unsupported_media_type", "Unsupported media type.") def render("429.json", _assigns), do: error_body(429, "too_many_requests", "Rate limit exceeded.") def render("500.json", _assigns), do: error_body(500, "internal_server_error", "Something went wrong.") def render(template, _assigns) do [code | _] = String.split(template, ".") status = String.to_integer(code) error_body(status, status |> Status.reason_atom() |> Atom.to_string(), "") end defp error_body(status, title, detail) do %{type: "about:blank", title: title, status: status, detail: detail} end end