towerops/lib/towerops_web/controllers/api/error_helpers.ex
Graham McIntire 532d88ffb9
perf: disable Req retry for faster error tests
- Add retry: false to VISP Client HTTP requests
- Add retry: false to ReleaseChecker GitHub API requests
- Remove unused Plug.Conn import from RemoteIpLogger
- Remove unused default parameter from req_get/2

Results:
- VISP sync test: 7007ms → 113ms (62x faster)
- ReleaseChecker test: 7004ms → 17ms (402x faster)
- UserResetPasswordLive test: 1495ms → 284ms (5x faster)

Req's default retry behavior (1s, 2s, 4s exponential backoff) was
causing 7-second delays for HTTP 500/503 error responses in tests.
For these clients, immediate failure is preferred over retries.
2026-03-10 16:19:31 -05:00

36 lines
1.2 KiB
Elixir

defmodule ToweropsWeb.Api.ErrorHelpers do
@moduledoc "Shared error translation helpers for API controllers."
@doc """
Translates changeset errors into a map of field names to error message lists.
Interpolates message parameters (e.g. `%{count}`) safely, validating keys
before atom conversion to prevent atom exhaustion attacks.
"""
@spec translate_errors(Ecto.Changeset.t()) :: %{atom() => [String.t()]}
def translate_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
safe_translate_key(key, opts)
end)
end)
end
@doc """
Formats translated changeset errors as a single string for GraphQL responses.
"""
@spec format_errors(Ecto.Changeset.t()) :: String.t()
def format_errors(changeset) do
changeset
|> translate_errors()
|> Enum.map_join("; ", fn {field, messages} -> "#{field}: #{Enum.join(messages, ", ")}" end)
end
defp safe_translate_key(key, opts) do
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
else
key
end
end
end