From e404d4eaf443f99979c4ab0c10022d3b91f9d7a3 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Fri, 26 Jun 2026 14:48:53 -0700 Subject: [PATCH] feat: add TelegramEx.Error struct for API errors Signed-off-by: Sai Asish Y --- lib/api.ex | 8 ++--- lib/error.ex | 77 +++++++++++++++++++++++++++++++++++++++++++++ test/error_test.exs | 29 +++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 lib/error.ex create mode 100644 test/error_test.exs diff --git a/lib/api.ex b/lib/api.ex index 08ad1aa..4e96f82 100644 --- a/lib/api.ex +++ b/lib/api.ex @@ -89,9 +89,9 @@ defmodule TelegramEx.API do {:ok, %{status: 200, body: %{"ok" => true, "result" => updates}}} -> {:ok, updates} - {:ok, %{body: %{"description" => reason}}} -> + {:ok, %{body: %{"description" => reason} = body}} -> Logger.error(reason) - {:error, :bad_request} + {:error, TelegramEx.Error.from_body(body)} {:error, reason} -> {:error, reason} @@ -209,9 +209,9 @@ defmodule TelegramEx.API do :ok end - defp handle_response({:ok, %{body: %{"description" => reason}}}) do + defp handle_response({:ok, %{body: %{"description" => reason} = body}}) do Logger.error(reason) - {:error, :bad_request} + {:error, TelegramEx.Error.from_body(body)} end defp handle_response({:ok, response}) do diff --git a/lib/error.ex b/lib/error.ex new file mode 100644 index 0000000..73327f9 --- /dev/null +++ b/lib/error.ex @@ -0,0 +1,77 @@ +defmodule TelegramEx.Error do + @moduledoc """ + Struct representing an error returned by the Telegram Bot API. + + When a request fails, Telegram responds with `"ok" => false` and a body + describing the problem. This struct captures that response in a structured + form so callers can inspect the error code, description, and any retry hint + instead of receiving a single opaque `:bad_request` atom. + + ## Fields + + - `:type` - Source of the error (`:telegram` for API errors) + - `:code` - Numeric error code from `error_code` (nil if absent) + - `:reason` - Optional atom describing the error (nil unless provided) + - `:description` - Human-readable description from the API + - `:retry_after` - Seconds to wait before retrying, from `parameters.retry_after` (nil if absent) + - `:raw` - The raw response body, for callers that need the full payload + + ## Examples + + case API.request(ctx) do + :ok -> + :ok + + {:error, %TelegramEx.Error{code: 429, retry_after: seconds}} -> + Process.sleep(seconds * 1000) + end + """ + + @typedoc """ + Error struct type. + + Carries the structured details of a failed Telegram API request. + """ + @type t :: %__MODULE__{ + type: atom(), + code: integer() | nil, + reason: atom() | nil, + description: String.t() | nil, + retry_after: integer() | nil, + raw: map() | nil + } + + defstruct type: :telegram, + code: nil, + reason: nil, + description: nil, + retry_after: nil, + raw: nil + + @doc """ + Builds an `Error` struct from a raw Telegram API response body. + + ## Parameters + + - `body` - Raw response body map from Telegram API + + ## Returns + + A `TelegramEx.Error` struct. + + ## Examples + + iex> TelegramEx.Error.from_body(%{"error_code" => 429, "description" => "Too Many Requests", "parameters" => %{"retry_after" => 5}}) + %TelegramEx.Error{type: :telegram, code: 429, description: "Too Many Requests", retry_after: 5, raw: %{"error_code" => 429, "description" => "Too Many Requests", "parameters" => %{"retry_after" => 5}}} + """ + @spec from_body(map()) :: t() + def from_body(body) when is_map(body) do + %__MODULE__{ + type: :telegram, + code: body["error_code"], + description: body["description"], + retry_after: get_in(body, ["parameters", "retry_after"]), + raw: body + } + end +end diff --git a/test/error_test.exs b/test/error_test.exs new file mode 100644 index 0000000..b9c45f5 --- /dev/null +++ b/test/error_test.exs @@ -0,0 +1,29 @@ +defmodule TelegramEx.ErrorTest do + use ExUnit.Case + doctest TelegramEx.Error + + alias TelegramEx.Error + + test "from_body/1 captures code, description and retry_after" do + body = %{ + "ok" => false, + "error_code" => 429, + "description" => "Too Many Requests: retry after 5", + "parameters" => %{"retry_after" => 5} + } + + assert %Error{ + type: :telegram, + code: 429, + description: "Too Many Requests: retry after 5", + retry_after: 5, + raw: ^body + } = Error.from_body(body) + end + + test "from_body/1 leaves retry_after nil when parameters are absent" do + body = %{"error_code" => 400, "description" => "Bad Request: message is not modified"} + + assert %Error{code: 400, retry_after: nil} = Error.from_body(body) + end +end