Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions lib/error.ex
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions test/error_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading