ML-21: Classify API errors as transient vs permanent

This commit is contained in:
Claudio Ortolina
2026-04-24 13:55:00 +01:00
parent be4f4686f8
commit a1c665b490
43 changed files with 1429 additions and 135 deletions
+60
View File
@@ -0,0 +1,60 @@
defmodule BraveSearch.API.ErrorResponse do
@moduledoc """
Structured error response for Brave Search API calls.
Brave Search returns a consistent JSON envelope on errors:
%{"type" => "ErrorResponse",
"error" => %{"status" => 422, "code" => "SUBSCRIPTION_TOKEN_INVALID",
"detail" => "...", "meta" => %{...}}}
HTTP status codes are the primary classifier (429 for rate limit, 5xx for
server errors). Brave uses 422 for most validation failures including
authentication errors that other APIs would return as 401.
"""
@behaviour MusicLibrary.ErrorResponse
alias MusicLibrary.HttpError
@type t :: %__MODULE__{
status: integer() | nil,
code: String.t() | nil,
message: String.t() | nil,
kind: HttpError.kind(),
body: term()
}
defstruct [:status, :code, :message, :kind, :body]
@spec from_response(Req.Response.t() | map()) :: t()
def from_response(%{status: status, body: body} = _response) do
%__MODULE__{
status: status,
code: extract_code(body),
message: extract_message(body),
kind: HttpError.default_kind(status),
body: body
}
end
@impl MusicLibrary.ErrorResponse
@spec retryable?(t()) :: boolean()
def retryable?(%__MODULE__{kind: kind}) when kind in [:rate_limit, :server_error, :timeout],
do: true
def retryable?(%__MODULE__{}), do: false
@impl MusicLibrary.ErrorResponse
@spec retry_delay_seconds(t()) :: pos_integer()
def retry_delay_seconds(%__MODULE__{kind: :rate_limit}), do: 60
def retry_delay_seconds(%__MODULE__{kind: :server_error}), do: 30
def retry_delay_seconds(%__MODULE__{kind: :timeout}), do: 10
def retry_delay_seconds(%__MODULE__{}), do: 30
defp extract_code(%{"error" => %{"code" => code}}) when is_binary(code), do: code
defp extract_code(_), do: nil
defp extract_message(%{"error" => %{"detail" => msg}}) when is_binary(msg), do: msg
defp extract_message(_), do: nil
end