Adds comprehensive Discord API error code mapping
Implements specific exception classes for all Discord API error codes to provide more granular error handling and better debugging experience. Creates individual exception classes for each Discord error code (10001-520006) with descriptive names and documentation strings that include the official error messages and codes. Updates HTTP client to automatically map Discord error codes to their corresponding exception classes, allowing developers to catch specific error types rather than generic HTTP exceptions. Includes comprehensive test coverage for the new error code mapping functionality to ensure correct exception raising based on API responses.
This commit is contained in:
parent
73416858ae
commit
66288ba920
File diff suppressed because it is too large
Load Diff
@ -11,12 +11,7 @@ import json
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
from typing import Optional, Dict, Any, Union, TYPE_CHECKING, List
|
from typing import Optional, Dict, Any, Union, TYPE_CHECKING, List
|
||||||
|
|
||||||
from .errors import (
|
from .errors import * # Import all custom exceptions
|
||||||
HTTPException,
|
|
||||||
RateLimitError,
|
|
||||||
AuthenticationError,
|
|
||||||
DisagreementException,
|
|
||||||
)
|
|
||||||
from . import __version__ # For User-Agent
|
from . import __version__ # For User-Agent
|
||||||
from .rate_limiter import RateLimiter
|
from .rate_limiter import RateLimiter
|
||||||
from .interactions import InteractionResponsePayload
|
from .interactions import InteractionResponsePayload
|
||||||
@ -31,6 +26,232 @@ API_BASE_URL = "https://discord.com/api/v10" # Using API v10
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DISCORD_ERROR_CODE_TO_EXCEPTION = {
|
||||||
|
0: GeneralError,
|
||||||
|
10001: UnknownAccount,
|
||||||
|
10002: UnknownApplication,
|
||||||
|
10003: UnknownChannel,
|
||||||
|
10004: UnknownGuild,
|
||||||
|
10005: UnknownIntegration,
|
||||||
|
10006: UnknownInvite,
|
||||||
|
10007: UnknownMember,
|
||||||
|
10008: UnknownMessage,
|
||||||
|
10009: UnknownPermissionOverwrite,
|
||||||
|
10010: UnknownProvider,
|
||||||
|
10011: UnknownRole,
|
||||||
|
10012: UnknownToken,
|
||||||
|
10013: UnknownUser,
|
||||||
|
10014: UnknownEmoji,
|
||||||
|
10015: UnknownWebhook,
|
||||||
|
10016: UnknownWebhookService,
|
||||||
|
10020: UnknownSession,
|
||||||
|
10021: UnknownAsset,
|
||||||
|
10026: UnknownBan,
|
||||||
|
10027: UnknownSKU,
|
||||||
|
10028: UnknownStoreListing,
|
||||||
|
10029: UnknownEntitlement,
|
||||||
|
10030: UnknownBuild,
|
||||||
|
10031: UnknownLobby,
|
||||||
|
10032: UnknownBranch,
|
||||||
|
10033: UnknownStoreDirectoryLayout,
|
||||||
|
10036: UnknownRedistributable,
|
||||||
|
10038: UnknownGiftCode,
|
||||||
|
10049: UnknownStream,
|
||||||
|
10050: UnknownPremiumServerSubscribeCooldown,
|
||||||
|
10057: UnknownGuildTemplate,
|
||||||
|
10059: UnknownDiscoverableServerCategory,
|
||||||
|
10060: UnknownSticker,
|
||||||
|
10061: UnknownStickerPack,
|
||||||
|
10062: UnknownInteraction,
|
||||||
|
10063: UnknownApplicationCommand,
|
||||||
|
10065: UnknownVoiceState,
|
||||||
|
10066: UnknownApplicationCommandPermissions,
|
||||||
|
10067: UnknownStageInstance,
|
||||||
|
10068: UnknownGuildMemberVerificationForm,
|
||||||
|
10069: UnknownGuildWelcomeScreen,
|
||||||
|
10070: UnknownGuildScheduledEvent,
|
||||||
|
10071: UnknownGuildScheduledEventUser,
|
||||||
|
10087: UnknownTag,
|
||||||
|
10097: UnknownSound,
|
||||||
|
20001: BotsCannotUseThisEndpoint,
|
||||||
|
20002: OnlyBotsCanUseThisEndpoint,
|
||||||
|
20009: ExplicitContentCannotBeSentToTheDesiredRecipients,
|
||||||
|
20012: NotAuthorizedToPerformThisActionOnThisApplication,
|
||||||
|
20016: ActionCannotBePerformedDueToSlowmodeRateLimit,
|
||||||
|
20018: OnlyTheOwnerOfThisAccountCanPerformThisAction,
|
||||||
|
20022: MessageCannotBeEditedDueToAnnouncementRateLimits,
|
||||||
|
20024: UnderMinimumAge,
|
||||||
|
20028: ChannelHitWriteRateLimit,
|
||||||
|
20029: ServerHitWriteRateLimit,
|
||||||
|
20031: DisallowedWordsInStageTopicOrNames,
|
||||||
|
20035: GuildPremiumSubscriptionLevelTooLow,
|
||||||
|
30001: MaximumNumberOfGuildsReached,
|
||||||
|
30002: MaximumNumberOfFriendsReached,
|
||||||
|
30003: MaximumNumberOfPinsReached,
|
||||||
|
30004: MaximumNumberOfRecipientsReached,
|
||||||
|
30005: MaximumNumberOfGuildRolesReached,
|
||||||
|
30007: MaximumNumberOfWebhooksReached,
|
||||||
|
30008: MaximumNumberOfEmojisReached,
|
||||||
|
30010: MaximumNumberOfReactionsReached,
|
||||||
|
30011: MaximumNumberOfGroupDMsReached,
|
||||||
|
30013: MaximumNumberOfGuildChannelsReached,
|
||||||
|
30015: MaximumNumberOfAttachmentsInAMessageReached,
|
||||||
|
30016: MaximumNumberOfInvitesReached,
|
||||||
|
30018: MaximumNumberOfAnimatedEmojisReached,
|
||||||
|
30019: MaximumNumberOfServerMembersReached,
|
||||||
|
30030: MaximumNumberOfServerCategoriesReached,
|
||||||
|
30031: GuildAlreadyHasATemplate,
|
||||||
|
30032: MaximumNumberOfApplicationCommandsReached,
|
||||||
|
30033: MaximumNumberOfThreadParticipantsReached,
|
||||||
|
30034: MaximumNumberOfDailyApplicationCommandCreatesReached,
|
||||||
|
30035: MaximumNumberOfBansForNonGuildMembersExceeded,
|
||||||
|
30037: MaximumNumberOfBansFetchesReached,
|
||||||
|
30038: MaximumNumberOfUncompletedGuildScheduledEventsReached,
|
||||||
|
30039: MaximumNumberOfStickersReached,
|
||||||
|
30040: MaximumNumberOfPruneRequestsReached,
|
||||||
|
30042: MaximumNumberOfGuildWidgetSettingsUpdatesReached,
|
||||||
|
30045: MaximumNumberOfSoundboardSoundsReached,
|
||||||
|
30046: MaximumNumberOfEditsToMessagesOlderThan1HourReached,
|
||||||
|
30047: MaximumNumberOfPinnedThreadsInAForumChannelReached,
|
||||||
|
30048: MaximumNumberOfTagsInAForumChannelReached,
|
||||||
|
30052: BitrateIsTooHighForChannelOfThisType,
|
||||||
|
30056: MaximumNumberOfPremiumEmojisReached,
|
||||||
|
30058: MaximumNumberOfWebhooksPerGuildReached,
|
||||||
|
30061: MaximumNumberOfChannelPermissionOverwritesReached,
|
||||||
|
30062: TheChannelsForThisGuildAreTooLarge,
|
||||||
|
40001: Unauthorized,
|
||||||
|
40002: YouNeedToVerifyYourAccount,
|
||||||
|
40003: YouAreOpeningDirectMessagesTooFast,
|
||||||
|
40004: SendMessagesHasBeenTemporarilyDisabled,
|
||||||
|
40005: RequestEntityTooLarge,
|
||||||
|
40006: ThisFeatureHasBeenTemporarilyDisabledServerSide,
|
||||||
|
40007: TheUserIsBannedFromThisGuild,
|
||||||
|
40012: ConnectionHasBeenRevoked,
|
||||||
|
40018: OnlyConsumableSKUsCanBeConsumed,
|
||||||
|
40019: YouCanOnlyDeleteSandboxEntitlements,
|
||||||
|
40032: TargetUserIsNotConnectedToVoice,
|
||||||
|
40033: ThisMessageHasAlreadyBeenCrossposted,
|
||||||
|
40041: AnApplicationCommandWithThatNameAlreadyExists,
|
||||||
|
40043: ApplicationInteractionFailedToSend,
|
||||||
|
40058: CannotSendAMessageInAForumChannel,
|
||||||
|
40060: InteractionHasAlreadyBeenAcknowledged,
|
||||||
|
40061: TagNamesMustBeUnique,
|
||||||
|
40062: ServiceResourceIsBeingRateLimited,
|
||||||
|
40066: ThereAreNoTagsAvailableThatCanBeSetByNonModerators,
|
||||||
|
40067: ATagIsRequiredToCreateAForumPostInThisChannel,
|
||||||
|
40074: AnEntitlementHasAlreadyBeenGrantedForThisResource,
|
||||||
|
40094: ThisInteractionHasHitTheMaximumNumberOfFollowUpMessages,
|
||||||
|
40333: CloudflareIsBlockingYourRequest,
|
||||||
|
50001: MissingAccess,
|
||||||
|
50002: InvalidAccountType,
|
||||||
|
50003: CannotExecuteActionOnADMChannel,
|
||||||
|
50004: GuildWidgetDisabled,
|
||||||
|
50005: CannotEditAMessageAuthoredByAnotherUser,
|
||||||
|
50006: CannotSendAnEmptyMessage,
|
||||||
|
50007: CannotSendMessagesToThisUser,
|
||||||
|
50008: CannotSendMessagesInANonTextChannel,
|
||||||
|
50009: ChannelVerificationLevelIsTooHighForYouToGainAccess,
|
||||||
|
50010: OAuth2ApplicationDoesNotHaveABot,
|
||||||
|
50011: OAuth2ApplicationLimitReached,
|
||||||
|
50012: InvalidOAuth2State,
|
||||||
|
50013: YouLackPermissionsToPerformThatAction,
|
||||||
|
50014: InvalidAuthenticationTokenProvided,
|
||||||
|
50015: NoteWasTooLong,
|
||||||
|
50016: ProvidedTooFewOrTooManyMessagesToDelete,
|
||||||
|
50017: InvalidMFALevel,
|
||||||
|
50019: AMessageCanOnlyBePinnedToTheChannelItWasSentIn,
|
||||||
|
50020: InviteCodeWasEitherInvalidOrTaken,
|
||||||
|
50021: CannotExecuteActionOnASystemMessage,
|
||||||
|
50024: CannotExecuteActionOnThisChannelType,
|
||||||
|
50025: InvalidOAuth2AccessTokenProvided,
|
||||||
|
50026: MissingRequiredOAuth2Scope,
|
||||||
|
50027: InvalidWebhookTokenProvided,
|
||||||
|
50028: InvalidRole,
|
||||||
|
50033: InvalidRecipients,
|
||||||
|
50034: AMessageProvidedWasTooOldToBulkDelete,
|
||||||
|
50035: InvalidFormBody,
|
||||||
|
50036: AnInviteWasAcceptedToAGuildTheApplicationBotIsNotIn,
|
||||||
|
50039: InvalidActivityAction,
|
||||||
|
50041: InvalidAPIVersionProvided,
|
||||||
|
50045: FileUploadedExceedsTheMaximumSize,
|
||||||
|
50046: InvalidFileUploaded,
|
||||||
|
50054: CannotSelfRedeemThisGift,
|
||||||
|
50055: InvalidGuild,
|
||||||
|
50057: InvalidSKU,
|
||||||
|
50067: InvalidRequestOrigin,
|
||||||
|
50068: InvalidMessageType,
|
||||||
|
50070: PaymentSourceRequiredToRedeemGift,
|
||||||
|
50073: CannotModifyASystemWebhook,
|
||||||
|
50074: CannotDeleteAChannelRequiredForCommunityGuilds,
|
||||||
|
50080: CannotEditStickersWithinAMessage,
|
||||||
|
50081: InvalidStickerSent,
|
||||||
|
50083: TriedToPerformAnOperationOnAnArchivedThread,
|
||||||
|
50085: InvalidThreadNotificationSettings,
|
||||||
|
50086: BeforeValueIsEarlierThanTheThreadCreationDate,
|
||||||
|
50087: CommunityServerChannelsMustBeTextChannels,
|
||||||
|
50091: TheEntityTypeOfTheEventIsDifferentFromTheEntityYouAreTryingToStartTheEventFor,
|
||||||
|
50095: ThisServerIsNotAvailableInYourLocation,
|
||||||
|
50097: ThisServerNeedsMonetizationEnabledInOrderToPerformThisAction,
|
||||||
|
50101: ThisServerNeedsMoreBoostsToPerformThisAction,
|
||||||
|
50109: TheRequestBodyContainsInvalidJSON,
|
||||||
|
50110: TheProvidedFileIsInvalid,
|
||||||
|
50123: TheProvidedFileTypeIsInvalid,
|
||||||
|
50124: TheProvidedFileDurationExceedsMaximumOf52Seconds,
|
||||||
|
50131: OwnerCannotBePendingMember,
|
||||||
|
50132: OwnershipCannotBeTransferredToABotUser,
|
||||||
|
50138: FailedToResizeAssetBelowTheMaximumSize,
|
||||||
|
50144: CannotMixSubscriptionAndNonSubscriptionRolesForAnEmoji,
|
||||||
|
50145: CannotConvertBetweenPremiumEmojiAndNormalEmoji,
|
||||||
|
50146: UploadedFileNotFound,
|
||||||
|
50151: TheSpecifiedEmojiIsInvalid,
|
||||||
|
50159: VoiceMessagesDoNotSupportAdditionalContent,
|
||||||
|
50160: VoiceMessagesMustHaveASingleAudioAttachment,
|
||||||
|
50161: VoiceMessagesMustHaveSupportingMetadata,
|
||||||
|
50162: VoiceMessagesCannotBeEdited,
|
||||||
|
50163: CannotDeleteGuildSubscriptionIntegration,
|
||||||
|
50173: YouCannotSendVoiceMessagesInThisChannel,
|
||||||
|
50178: TheUserAccountMustFirstBeVerified,
|
||||||
|
50192: TheProvidedFileDoesNotHaveAValidDuration,
|
||||||
|
50600: YouDoNotHavePermissionToSendThisSticker,
|
||||||
|
60003: TwoFactorIsRequiredForThisOperation,
|
||||||
|
80004: NoUsersWithDiscordTagExist,
|
||||||
|
90001: ReactionWasBlocked,
|
||||||
|
90002: UserCannotUseBurstReactions,
|
||||||
|
110001: ApplicationNotYetAvailable,
|
||||||
|
130000: APIResourceIsCurrentlyOverloaded,
|
||||||
|
150006: TheStageIsAlreadyOpen,
|
||||||
|
160002: CannotReplyWithoutPermissionToReadMessageHistory,
|
||||||
|
160004: AThreadHasAlreadyBeenCreatedForThisMessage,
|
||||||
|
160005: ThreadIsLocked,
|
||||||
|
160006: MaximumNumberOfActiveThreadsReached,
|
||||||
|
160007: MaximumNumberOfActiveAnnouncementThreadsReached,
|
||||||
|
170001: InvalidJSONForUploadedLottieFile,
|
||||||
|
170002: UploadedLottiesCannotContainRasterizedImages,
|
||||||
|
170003: StickerMaximumFramerateExceeded,
|
||||||
|
170004: StickerFrameCountExceedsMaximumOf1000Frames,
|
||||||
|
170005: LottieAnimationMaximumDimensionsExceeded,
|
||||||
|
170006: StickerFrameRateIsEitherTooSmallOrTooLarge,
|
||||||
|
170007: StickerAnimationDurationExceedsMaximumOf5Seconds,
|
||||||
|
180000: CannotUpdateAFinishedEvent,
|
||||||
|
180002: FailedToCreateStageNeededForStageEvent,
|
||||||
|
200000: MessageWasBlockedByAutomaticModeration,
|
||||||
|
200001: TitleWasBlockedByAutomaticModeration,
|
||||||
|
220001: WebhooksPostedToForumChannelsMustHaveAThreadNameOrThreadId,
|
||||||
|
220002: WebhooksPostedToForumChannelsCannotHaveBothAThreadNameAndThreadId,
|
||||||
|
220003: WebhooksCanOnlyCreateThreadsInForumChannels,
|
||||||
|
220004: WebhookServicesCannotBeUsedInForumChannels,
|
||||||
|
240000: MessageBlockedByHarmfulLinksFilter,
|
||||||
|
350000: CannotEnableOnboardingRequirementsAreNotMet,
|
||||||
|
350001: CannotUpdateOnboardingWhileBelowRequirements,
|
||||||
|
500000: FailedToBanUsers,
|
||||||
|
520000: PollVotingBlocked,
|
||||||
|
520001: PollExpired,
|
||||||
|
520002: InvalidChannelTypeForPollCreation,
|
||||||
|
520003: CannotEditAPollMessage,
|
||||||
|
520004: CannotUseAnEmojiIncludedWithThePoll,
|
||||||
|
520006: CannotExpireANonPollMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient:
|
class HTTPClient:
|
||||||
"""Handles HTTP requests to the Discord API."""
|
"""Handles HTTP requests to the Discord API."""
|
||||||
@ -208,6 +429,17 @@ class HTTPClient:
|
|||||||
discord_error_code = (
|
discord_error_code = (
|
||||||
data.get("code") if isinstance(data, dict) else None
|
data.get("code") if isinstance(data, dict) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if discord_error_code in DISCORD_ERROR_CODE_TO_EXCEPTION:
|
||||||
|
exc_class = DISCORD_ERROR_CODE_TO_EXCEPTION[discord_error_code]
|
||||||
|
raise exc_class(
|
||||||
|
response,
|
||||||
|
f"API Error on {method} {endpoint}: {error_text}",
|
||||||
|
status=response.status,
|
||||||
|
text=error_text,
|
||||||
|
error_code=discord_error_code,
|
||||||
|
)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
response,
|
response,
|
||||||
f"API Error on {method} {endpoint}: {error_text}",
|
f"API Error on {method} {endpoint}: {error_text}",
|
||||||
|
@ -1,12 +1,91 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import aiohttp
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
from disagreement.errors import (
|
from disagreement.errors import (
|
||||||
HTTPException,
|
HTTPException,
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
AppCommandOptionConversionError,
|
AppCommandOptionConversionError,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
NotFound,
|
NotFound,
|
||||||
|
UnknownAccount,
|
||||||
|
MaximumNumberOfGuildsReached,
|
||||||
)
|
)
|
||||||
|
from disagreement.http import HTTPClient
|
||||||
|
|
||||||
|
|
||||||
|
# A fixture to provide an HTTPClient with a mocked session
|
||||||
|
@pytest.fixture
|
||||||
|
def http_client():
|
||||||
|
# Using a real session and patching the request method is more robust
|
||||||
|
client = HTTPClient(token="fake_token")
|
||||||
|
yield client
|
||||||
|
# Cleanup: close the session after the test
|
||||||
|
# This requires making the fixture async or running this in an event loop
|
||||||
|
async def close_session():
|
||||||
|
if client._session:
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
loop.run_until_complete(close_session())
|
||||||
|
except RuntimeError:
|
||||||
|
asyncio.run(close_session())
|
||||||
|
|
||||||
|
|
||||||
|
# Mock aiohttp response
|
||||||
|
class MockAiohttpResponse:
|
||||||
|
def __init__(self, status, json_data, headers=None):
|
||||||
|
self.status = status
|
||||||
|
self._json_data = json_data
|
||||||
|
self.headers = headers or {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return self._json_data
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return str(self._json_data)
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"error_code, error_message, expected_exception",
|
||||||
|
[
|
||||||
|
(10001, "Unknown account", UnknownAccount),
|
||||||
|
(30001, "Maximum number of guilds reached", MaximumNumberOfGuildsReached),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_error_code_mapping_raises_correct_exception(
|
||||||
|
http_client, error_code, error_message, expected_exception
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Tests if the HTTP client correctly raises a specific exception
|
||||||
|
based on the Discord error code.
|
||||||
|
"""
|
||||||
|
mock_response = MockAiohttpResponse(
|
||||||
|
status=400, json_data={"code": error_code, "message": error_message}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Patch the session object to control the response
|
||||||
|
with patch("aiohttp.ClientSession") as mock_session_class:
|
||||||
|
mock_session_instance = mock_session_class.return_value
|
||||||
|
mock_session_instance.request.return_value = mock_response
|
||||||
|
|
||||||
|
# Assert that the correct exception is raised
|
||||||
|
with pytest.raises(expected_exception) as excinfo:
|
||||||
|
await http_client.request("GET", "/test-endpoint")
|
||||||
|
|
||||||
|
# Optionally, check the exception details
|
||||||
|
assert excinfo.value.status == 400
|
||||||
|
assert excinfo.value.error_code == error_code
|
||||||
|
assert error_message in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
def test_http_exception_message():
|
def test_http_exception_message():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user