errors: refactor

pull/452/head
Demian 2 years ago
parent 10b70148f7
commit 9805b1f622

@ -70,5 +70,5 @@ func TestRaw(t *testing.T) {
assert.EqualError(t, err, "telebot: "+io.ErrUnexpectedEOF.Error()) assert.EqualError(t, err, "telebot: "+io.ErrUnexpectedEOF.Error())
_, err = b.Raw("testUnknownError", nil) _, err = b.Raw("testUnknownError", nil)
assert.EqualError(t, err, "telegram unknown: unknown error (400)") assert.EqualError(t, err, "telegram: unknown error (400)")
} }

@ -5,26 +5,32 @@ import (
"strings" "strings"
) )
type APIError struct { type (
Code int Error struct {
Description string Code int
Message string Description string
Parameters map[string]interface{} Message string
} }
type FloodError struct { FloodError struct {
*APIError err *Error
RetryAfter int RetryAfter int
} }
GroupError struct {
err *Error
MigratedTo int64
}
)
// ʔ returns description of error. // ʔ returns description of error.
// A tiny shortcut to make code clearer. // A tiny shortcut to make code clearer.
func (err *APIError) ʔ() string { func (err *Error) ʔ() string {
return err.Description return err.Description
} }
// Error implements error interface. // Error implements error interface.
func (err *APIError) Error() string { func (err *Error) Error() string {
msg := err.Message msg := err.Message
if msg == "" { if msg == "" {
split := strings.Split(err.Description, ": ") split := strings.Split(err.Description, ": ")
@ -37,10 +43,20 @@ func (err *APIError) Error() string {
return fmt.Sprintf("telegram: %s (%d)", msg, err.Code) return fmt.Sprintf("telegram: %s (%d)", msg, err.Code)
} }
// NewAPIError returns new APIError instance with given description. // Error implements error interface.
func (err FloodError) Error() string {
return err.err.Error()
}
// Error implements error interface.
func (err GroupError) Error() string {
return err.err.Error()
}
// NewError returns new Error instance with given description.
// First element of msgs is Description. The second is optional Message. // First element of msgs is Description. The second is optional Message.
func NewAPIError(code int, msgs ...string) *APIError { func NewError(code int, msgs ...string) *Error {
err := &APIError{Code: code} err := &Error{Code: code}
if len(msgs) >= 1 { if len(msgs) >= 1 {
err.Description = msgs[0] err.Description = msgs[0]
} }
@ -50,57 +66,63 @@ func NewAPIError(code int, msgs ...string) *APIError {
return err return err
} }
// General errors
var ( var (
// General errors ErrUnauthorized = NewError(401, "Unauthorized")
ErrUnauthorized = NewAPIError(401, "Unauthorized") ErrNotStartedByUser = NewError(403, "Forbidden: bot can't initiate conversation with a user")
ErrNotStartedByUser = NewAPIError(403, "Forbidden: bot can't initiate conversation with a user") ErrBlockedByUser = NewError(401, "Forbidden: bot was blocked by the user")
ErrBlockedByUser = NewAPIError(401, "Forbidden: bot was blocked by the user") ErrUserIsDeactivated = NewError(401, "Forbidden: user is deactivated")
ErrUserIsDeactivated = NewAPIError(401, "Forbidden: user is deactivated") ErrNotFound = NewError(404, "Not Found")
ErrNotFound = NewAPIError(404, "Not Found") ErrInternal = NewError(500, "Internal Server Error")
ErrInternal = NewAPIError(500, "Internal Server Error") )
// Bad request errors // Bad request errors
ErrTooLarge = NewAPIError(400, "Request Entity Too Large") var (
ErrMessageTooLong = NewAPIError(400, "Bad Request: message is too long") ErrTooLarge = NewError(400, "Request Entity Too Large")
ErrToForwardNotFound = NewAPIError(400, "Bad Request: message to forward not found") ErrMessageTooLong = NewError(400, "Bad Request: message is too long")
ErrToReplyNotFound = NewAPIError(400, "Bad Request: reply message not found") ErrToForwardNotFound = NewError(400, "Bad Request: message to forward not found")
ErrToDeleteNotFound = NewAPIError(400, "Bad Request: message to delete not found") ErrToReplyNotFound = NewError(400, "Bad Request: reply message not found")
ErrEmptyMessage = NewAPIError(400, "Bad Request: message must be non-empty") ErrToDeleteNotFound = NewError(400, "Bad Request: message to delete not found")
ErrEmptyText = NewAPIError(400, "Bad Request: text is empty") ErrEmptyMessage = NewError(400, "Bad Request: message must be non-empty")
ErrEmptyChatID = NewAPIError(400, "Bad Request: chat_id is empty") ErrEmptyText = NewError(400, "Bad Request: text is empty")
ErrChatNotFound = NewAPIError(400, "Bad Request: chat not found") ErrEmptyChatID = NewError(400, "Bad Request: chat_id is empty")
ErrMessageNotModified = NewAPIError(400, "Bad Request: message is not modified") ErrChatNotFound = NewError(400, "Bad Request: chat not found")
ErrSameMessageContent = NewAPIError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message") ErrMessageNotModified = NewError(400, "Bad Request: message is not modified")
ErrCantEditMessage = NewAPIError(400, "Bad Request: message can't be edited") ErrSameMessageContent = NewError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")
ErrButtonDataInvalid = NewAPIError(400, "Bad Request: BUTTON_DATA_INVALID") ErrCantEditMessage = NewError(400, "Bad Request: message can't be edited")
ErrWrongTypeOfContent = NewAPIError(400, "Bad Request: wrong type of the web page content") ErrButtonDataInvalid = NewError(400, "Bad Request: BUTTON_DATA_INVALID")
ErrBadURLContent = NewAPIError(400, "Bad Request: failed to get HTTP URL content") ErrWrongTypeOfContent = NewError(400, "Bad Request: wrong type of the web page content")
ErrWrongFileID = NewAPIError(400, "Bad Request: wrong file identifier/HTTP URL specified") ErrBadURLContent = NewError(400, "Bad Request: failed to get HTTP URL content")
ErrWrongFileIDSymbol = NewAPIError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol") ErrWrongFileID = NewError(400, "Bad Request: wrong file identifier/HTTP URL specified")
ErrWrongFileIDLength = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong string length") ErrWrongFileIDSymbol = NewError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol")
ErrWrongFileIDCharacter = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong character in the string") ErrWrongFileIDLength = NewError(400, "Bad Request: wrong remote file id specified: Wrong string length")
ErrWrongFileIDPadding = NewAPIError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string") ErrWrongFileIDCharacter = NewError(400, "Bad Request: wrong remote file id specified: Wrong character in the string")
ErrFailedImageProcess = NewAPIError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed") ErrWrongFileIDPadding = NewError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string")
ErrInvalidStickerSet = NewAPIError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid") ErrFailedImageProcess = NewError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed")
ErrBadPollOptions = NewAPIError(400, "Bad Request: expected an Array of String as options") ErrInvalidStickerSet = NewError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid")
ErrGroupMigrated = NewAPIError(400, "Bad Request: group chat was upgraded to a supergroup chat") ErrBadPollOptions = NewError(400, "Bad Request: expected an Array of String as options")
ErrGroupMigrated = NewError(400, "Bad Request: group chat was upgraded to a supergroup chat")
)
// No rights errors // No rights errors
ErrNoRightsToRestrict = NewAPIError(400, "Bad Request: not enough rights to restrict/unrestrict chat member") var (
ErrNoRightsToSend = NewAPIError(400, "Bad Request: have no rights to send a message") ErrNoRightsToRestrict = NewError(400, "Bad Request: not enough rights to restrict/unrestrict chat member")
ErrNoRightsToSendPhoto = NewAPIError(400, "Bad Request: not enough rights to send photos to the chat") ErrNoRightsToSend = NewError(400, "Bad Request: have no rights to send a message")
ErrNoRightsToSendStickers = NewAPIError(400, "Bad Request: not enough rights to send stickers to the chat") ErrNoRightsToSendPhoto = NewError(400, "Bad Request: not enough rights to send photos to the chat")
ErrNoRightsToSendGifs = NewAPIError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat") ErrNoRightsToSendStickers = NewError(400, "Bad Request: not enough rights to send stickers to the chat")
ErrNoRightsToDelete = NewAPIError(400, "Bad Request: message can't be deleted") ErrNoRightsToSendGifs = NewError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat")
ErrKickingChatOwner = NewAPIError(400, "Bad Request: can't remove chat owner") ErrNoRightsToDelete = NewError(400, "Bad Request: message can't be deleted")
ErrKickingChatOwner = NewError(400, "Bad Request: can't remove chat owner")
)
// Super/groups errors // Super/groups errors
ErrBotKickedFromGroup = NewAPIError(403, "Forbidden: bot was kicked from the group chat") var (
ErrBotKickedFromSuperGroup = NewAPIError(403, "Forbidden: bot was kicked from the supergroup chat") ErrBotKickedFromGroup = NewError(403, "Forbidden: bot was kicked from the group chat")
ErrBotKickedFromSuperGroup = NewError(403, "Forbidden: bot was kicked from the supergroup chat")
) )
// ErrByDescription returns APIError instance by given description. // Err returns Error instance by given description.
func ErrByDescription(s string) error { func Err(s string) error {
switch s { switch s {
case ErrUnauthorized.ʔ(): case ErrUnauthorized.ʔ():
return ErrUnauthorized return ErrUnauthorized

@ -57,52 +57,49 @@ func wrapError(err error) error {
// In other cases it extracts API error. If error is not presented // In other cases it extracts API error. If error is not presented
// in errors.go, it will be prefixed with `unknown` keyword. // in errors.go, it will be prefixed with `unknown` keyword.
func extractOk(data []byte) error { func extractOk(data []byte) error {
// Parse the error message as JSON var e struct {
var tgramApiError struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
ErrorCode int `json:"error_code"` Code int `json:"error_code"`
Description string `json:"description"` Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"` Parameters map[string]interface{} `json:"parameters"`
} }
jdecoder := json.NewDecoder(bytes.NewReader(data)) if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
jdecoder.UseNumber() return nil // FIXME
err := jdecoder.Decode(&tgramApiError)
if err != nil {
//return errors.Wrap(err, "can't parse JSON reply, the Telegram server is mibehaving")
// FIXME / TODO: in this case the error might be at HTTP level, or the content is not JSON (eg. image?)
return nil
} }
if e.Ok {
if tgramApiError.Ok {
// No error
return nil return nil
} }
err = ErrByDescription(tgramApiError.Description) err := Err(e.Description)
if err != nil { switch err {
apierr, _ := err.(*APIError) case nil:
// Formally this is wrong, as the error is not created on the fly case ErrGroupMigrated:
// However, given the current way of handling errors, this a working migratedTo, ok := e.Parameters["migrate_to_chat_id"]
// workaround which doesn't break the API if !ok {
apierr.Parameters = tgramApiError.Parameters return NewError(e.Code, e.Description)
return apierr }
return GroupError{
err: err.(*Error),
MigratedTo: int64(migratedTo.(float64)),
}
default:
return err
} }
switch tgramApiError.ErrorCode { switch e.Code {
case http.StatusTooManyRequests: case http.StatusTooManyRequests:
retryAfter, ok := tgramApiError.Parameters["retry_after"] retryAfter, ok := e.Parameters["retry_after"]
if !ok { if !ok {
return NewAPIError(429, tgramApiError.Description) return NewError(e.Code, e.Description)
} }
retryAfterInt, _ := strconv.Atoi(fmt.Sprint(retryAfter))
err = FloodError{ err = FloodError{
APIError: NewAPIError(429, tgramApiError.Description), err: NewError(e.Code, e.Description),
RetryAfter: retryAfterInt, RetryAfter: int(retryAfter.(float64)),
} }
default: default:
err = fmt.Errorf("telegram unknown: %s (%d)", tgramApiError.Description, tgramApiError.ErrorCode) err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
} }
return err return err

@ -8,20 +8,37 @@ import (
) )
func TestExtractOk(t *testing.T) { func TestExtractOk(t *testing.T) {
data := []byte(`{"ok":true,"result":{"foo":"bar"}}`) data := []byte(`{"ok": true, "result": {}}`)
require.NoError(t, extractOk(data)) require.NoError(t, extractOk(data))
data = []byte(`{"ok":false,"error_code":400,"description":"Bad Request: reply message not found"}`) data = []byte(`{
"ok": false,
"error_code": 400,
"description": "Bad Request: reply message not found"
}`)
assert.EqualError(t, extractOk(data), ErrToReplyNotFound.Error()) assert.EqualError(t, extractOk(data), ErrToReplyNotFound.Error())
data = []byte(`{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 8","parameters":{"retry_after":8}}`) data = []byte(`{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 8",
"parameters": {"retry_after": 8}
}`)
assert.Equal(t, FloodError{ assert.Equal(t, FloodError{
APIError: NewAPIError(429, "Too Many Requests: retry after 8"), err: NewError(429, "Too Many Requests: retry after 8"),
RetryAfter: 8, RetryAfter: 8,
}, extractOk(data)) }, extractOk(data))
data = []byte(`{"ok":false,"error_code":400,"description":"Bad Request: group chat was upgraded to a supergroup chat","parameters":{"migrate_to_chat_id": -1234}}`) data = []byte(`{
assert.EqualError(t, extractOk(data), ErrGroupMigrated.Error()) "ok": false,
"error_code": 400,
"description": "Bad Request: group chat was upgraded to a supergroup chat",
"parameters": {"migrate_to_chat_id": -100123456789}
}`)
assert.Equal(t, GroupError{
err: ErrGroupMigrated,
MigratedTo: -100123456789,
}, extractOk(data))
} }
func TestExtractMessage(t *testing.T) { func TestExtractMessage(t *testing.T) {

Loading…
Cancel
Save