Browse Source

Implement Better Discord Errors (#291)

* Initial error parsing

* Implement better errors

* Add missing error codes

* Add voice disconnect opcodes

* Remove unused class, add summaries to discordjsonerror, and remove public constructor of slash command properties

* Add error code summary

* Update error message summary

* Update src/Discord.Net.Core/DiscordJsonError.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Update src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Fix autocomplete result value

Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com>
pull/1923/head
Quin Lynch GitHub 3 years ago
parent
commit
9cc7d2deaa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 478 additions and 73 deletions
  1. +197
    -0
      src/Discord.Net.Core/DiscordErrorCode.cs
  2. +53
    -0
      src/Discord.Net.Core/DiscordJsonError.cs
  3. +6
    -51
      src/Discord.Net.Core/Net/ApplicationCommandException.cs
  4. +8
    -9
      src/Discord.Net.Core/Net/HttpException.cs
  5. +20
    -0
      src/Discord.Net.Rest/API/Common/DiscordError.cs
  6. +12
    -0
      src/Discord.Net.Rest/API/Common/Error.cs
  7. +17
    -0
      src/Discord.Net.Rest/API/Common/PropertyErrorDescription.cs
  8. +2
    -2
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  9. +5
    -3
      src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs
  10. +88
    -0
      src/Discord.Net.Rest/Net/Converters/DiscordErrorConverter.cs
  11. +7
    -8
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  12. +63
    -0
      src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs

+ 197
- 0
src/Discord.Net.Core/DiscordErrorCode.cs View File

@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a set of json error codes received by discord.
/// </summary>
public enum DiscordErrorCode
{
GeneralError = 0,

#region UnknownXYZ (10XXX)
UnknownAccount = 10001,
UnknownApplication = 10002,
UnknownChannel = 10003,
UnknownGuild = 10004,
UnknownIntegration = 10005,
UnknownInvite = 10006,
UnknownMember = 10007,
UnknownMessage = 10008,
UnknownPermissionOverwrite = 10009,
UnknownProvider = 10010,
UnknownRole = 10011,
UnknownToken = 10012,
UnknownUser = 10013,
UnknownEmoji = 10014,
UnknownWebhook = 10015,
UnknownWebhookService = 10016,
UnknownSession = 10020,
UnknownBan = 10026,
UnknownSKU = 10027,
UnknownStoreListing = 10028,
UnknownEntitlement = 10029,
UnknownBuild = 10030,
UnknownLobby = 10031,
UnknownBranch = 10032,
UnknownStoreDirectoryLayout = 10033,
UnknownRedistributable = 10036,
UnknownGiftCode = 10038,
UnknownStream = 10049,
UnknownPremiumServerSubscribeCooldown = 10050,
UnknownGuildTemplate = 10057,
UnknownDiscoverableServerCategory = 10059,
UnknownSticker = 10060,
UnknownInteraction = 10062,
UnknownApplicationCommand = 10063,
UnknownApplicationCommandPermissions = 10066,
UnknownStageInstance = 10067,
UnknownGuildMemberVerificationForm = 10068,
UnknownGuildWelcomeScreen = 10069,
UnknownGuildScheduledEvent = 10070,
UnknownGuildScheduledEventUser = 10071,
#endregion

#region General Actions (20XXX)
BotsCannotUse = 20001,
OnlyBotsCanUse = 20002,
CannotSendExplicitContent = 20009,
ApplicationActionUnauthorized = 20012,
ActionSlowmode = 20016,
OnlyOwnerAction = 20018,
AnnouncementEditRatelimit = 20022,
ChannelWriteRatelimit = 20028,
WordsNotAllowed = 20031,
GuildPremiumTooLow = 20035,
#endregion

#region Numeric Limits Reached (30XXX)
MaximumGuildsReached = 30001,
MaximumFriendsReached = 30002,
MaximumPinsReached = 30003,
MaximumRecipientsReached = 30004,
MaximumGuildRolesReached = 30005,
MaximumWebhooksReached = 30007,
MaximumEmojisReached = 30008,
MaximumReactionsReached = 30010,
MaximumGuildChannelsReached = 30013,
MaximumAttachmentsReached = 30015,
MaximumInvitesReached = 30016,
MaximumAnimatedEmojisReached = 30018,
MaximumServerMembersReached = 30019,
MaximumServerCategoriesReached = 30030,
GuildTemplateAlreadyExists = 30031,
MaximumThreadMembersReached = 30033,
MaximumBansForNonGuildMembersReached = 30035,
MaximumBanFetchesReached = 30037,
MaximumUncompleteGuildScheduledEvents = 30038,
MaximumStickersReached = 30039,
MaximumPruneRequestReached = 30040,
MaximumGuildWigitsReached = 30042,
#endregion

#region General Request Errors (40XXX)
TokenUnauthorized = 40001,
InvalidVerification = 40002,
OpeningDMTooFast = 40003,
RequestEntityTooLarge = 40005,
FeatureDisabled = 40006,
UserBanned = 40007,
TargetUserNotInVoice = 40032,
MessageAlreadyCrossposted = 40033,
ApplicationNameAlreadyExists = 40041,
#endregion

#region Action Preconditions/Checks (50XXX)
MissingPermissions = 50001,
InvalidAccountType = 50002,
CannotExecuteForDM = 50003,
GuildWigitDisabled = 50004,
CannotEditOtherUsersMessage = 50005,
CannotSendEmptyMessage = 50006,
CannotSendMessageToUser = 50007,
CannotSendMessageToVoiceChannel = 50008,
ChannelVerificationTooHight = 50009,
OAuth2ApplicationDoesntHaveBot = 50010,
OAuth2ApplicationLimitReached = 50011,
InvalidOAuth2State = 50012,
InsufficientPermissions = 50013,
InvalidAuthenticationToken = 50014,
NoteTooLong = 50015,
ProvidedMessageDeleteCountOutOfBounds = 50016,
InvalidPinChannel = 50019,
InvalidInvite = 50020,
CannotExecuteOnSystemMessage = 50021,
CannotExecuteOnChannelType = 50024,
InvalidOAuth2Token = 50025,
MissingOAuth2Scope = 50026,
InvalidWebhookToken = 50027,
InvalidRole = 50028,
InvalidRecipients = 50033,
BulkDeleteMessageTooOld = 50034,
InvalidFormBody = 50035,
InviteAcceptedForGuildThatBotIsntIn = 50036,
InvalidAPIVersion = 50041,
FileUploadTooBig = 50045,
InvalidFileUpload = 50046,
CannotSelfRedeemGift = 50054,
PaymentSourceRequiredForGift = 50070,
CannotDeleteRequiredCommunityChannel = 50074,
InvalidSticker = 50081,
CannotExecuteOnArchivedThread = 50083,
InvalidThreadNotificationSettings = 50084,
BeforeValueEarlierThanThreadCreation = 50085,
ServerLocaleUnavailable = 50095,
ServerRequiresMonetization = 50097,
ServerRequiresBoosts = 50101,

#endregion

#region 2FA (60XXX)
Requires2FA = 60003,
#endregion

#region User Searches (80XXX)
NoUsersWithTag = 80004,
#endregion

#region Reactions (90XXX)
ReactionBlocked = 90001,
#endregion

#region API Status (130XXX)
APIOverloaded = 130000,
#endregion

#region Stage Errors (150XXX)
StageAlreadyOpened = 150006,
#endregion

#region Reply and Thread Errors (160XXX)
CannotReplyWithoutReadMessageHistory = 160002,
MessageAlreadyContainsThread = 160004,
ThreadIsLocked = 160005,
MaximumActiveThreadsReached = 160006,
MaximumAnnouncementThreadsReached = 160007,
#endregion

#region Sticker Uploads (170XXX)
InvalidJSONLottie = 170001,
LottieCantContainRasters = 170002,
StickerMaximumFramerateExceeded = 170003,
StickerMaximumFrameCountExceeded = 170004,
LottieMaximumDimentionsExceeded = 170005,
StickerFramerateBoundsExceeed = 170006,
StickerAnimationDurationTooLong = 170007,
#endregion

#region Guild Scheduled Events
CannotUpdateFinishedEvent = 180000,
FailedStageCreation = 180002,
#endregion
}
}

+ 53
- 0
src/Discord.Net.Core/DiscordJsonError.cs View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a generic parsed json error received from discord after performing a rest request.
/// </summary>
public struct DiscordJsonError
{
/// <summary>
/// Gets the json path of the error.
/// </summary>
public string Path { get; }

/// <summary>
/// Gets a collection of errors associated with the specific property at the path.
/// </summary>
public IReadOnlyCollection<DiscordError> Errors { get; }

internal DiscordJsonError(string path, DiscordError[] errors)
{
Path = path;
Errors = errors.ToImmutableArray();
}
}

/// <summary>
/// Represents an error with a property.
/// </summary>
public struct DiscordError
{
/// <summary>
/// Gets the code of the error.
/// </summary>
public string Code { get; }

/// <summary>
/// Gets the message describing what went wrong.
/// </summary>
public string Message { get; }

internal DiscordError(string code, string message)
{
Code = code;
Message = message;
}
}
}

+ 6
- 51
src/Discord.Net.Core/Net/ApplicationCommandException.cs View File

@@ -1,60 +1,15 @@
using System;
using System.Linq;

namespace Discord.Net
{
public class ApplicationCommandException : Exception
[Obsolete("Please use HttpException instead of this. Will be removed in next major version.", false)]
public class ApplicationCommandException : HttpException
{
/// <summary>
/// Gets the JSON error code returned by Discord.
/// </summary>
/// <returns>
/// A
/// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#json">JSON error code</see>
/// from Discord, or <c>null</c> if none.
/// </returns>
public int? DiscordCode { get; }

/// <summary>
/// Gets the reason of the exception.
/// </summary>
public string Reason { get; }

/// <summary>
/// Gets the request object used to send the request.
/// </summary>
public IRequest Request { get; }

/// <summary>
/// The error object returned from discord.
/// </summary>
/// <remarks>
/// Note: This object can be null if discord didn't provide it.
/// </remarks>
public object Error { get; }

/// <summary>
/// The request json used to create the application command. This is useful for checking your commands for any format errors.
/// </summary>
public string RequestJson { get; }

/// <summary>
/// The underlying <see cref="HttpException"/> that caused this exception to be thrown.
/// </summary>
public HttpException InnerHttpException { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationCommandException" /> class.
/// </summary>
/// <param name="requestJson"></param>
/// <param name="httpError"></param>
public ApplicationCommandException(string requestJson, HttpException httpError)
: base("The application command failed to be created!", httpError)
public ApplicationCommandException(HttpException httpError)
: base(httpError.HttpCode, httpError.Request, httpError.DiscordCode, httpError.Reason, httpError.Errors.ToArray())
{
Request = httpError.Request;
DiscordCode = httpError.DiscordCode;
Reason = httpError.Reason;
Error = httpError.Error;
RequestJson = requestJson;
InnerHttpException = httpError;
}
}
}

+ 8
- 9
src/Discord.Net.Core/Net/HttpException.cs View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Net;

namespace Discord.Net
@@ -25,7 +27,7 @@ namespace Discord.Net
/// <see href="https://discord.com/developers/docs/topics/opcodes-and-status-codes#json">JSON error code</see>
/// from Discord, or <c>null</c> if none.
/// </returns>
public int? DiscordCode { get; }
public DiscordErrorCode? DiscordCode { get; }
/// <summary>
/// Gets the reason of the exception.
/// </summary>
@@ -35,12 +37,9 @@ namespace Discord.Net
/// </summary>
public IRequest Request { get; }
/// <summary>
/// The error object returned from discord.
/// Gets a collection of json errors describing what went wrong with the request.
/// </summary>
/// <remarks>
/// Note: This object can be null if discord didn't provide it.
/// </remarks>
public object Error { get; }
public IReadOnlyCollection<DiscordJsonError> Errors { get; }

/// <summary>
/// Initializes a new instance of the <see cref="HttpException" /> class.
@@ -49,14 +48,14 @@ namespace Discord.Net
/// <param name="request">The request that was sent prior to the exception.</param>
/// <param name="discordCode">The Discord status code returned.</param>
/// <param name="reason">The reason behind the exception.</param>
public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null, object errors = null)
: base(CreateMessage(httpCode, discordCode, reason))
public HttpException(HttpStatusCode httpCode, IRequest request, DiscordErrorCode? discordCode = null, string reason = null, DiscordJsonError[] errors = null)
: base(CreateMessage(httpCode, (int?)discordCode, reason))
{
HttpCode = httpCode;
Request = request;
DiscordCode = discordCode;
Reason = reason;
Error = errors;
Errors = errors?.ToImmutableArray() ?? ImmutableArray<DiscordJsonError>.Empty;
}

private static string CreateMessage(HttpStatusCode httpCode, int? discordCode = null, string reason = null)


+ 20
- 0
src/Discord.Net.Rest/API/Common/DiscordError.cs View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
[JsonConverter(typeof(Discord.Net.Converters.DiscordErrorConverter))]
internal class DiscordError
{
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("code")]
public DiscordErrorCode Code { get; set; }
[JsonProperty("errors")]
public Optional<ErrorDetails[]> Errors { get; set; }
}
}

+ 12
- 0
src/Discord.Net.Rest/API/Common/Error.cs View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace Discord.API
{
internal class Error
{
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}
}

+ 17
- 0
src/Discord.Net.Rest/API/Common/PropertyErrorDescription.cs View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class ErrorDetails
{
[JsonProperty("name")]
public Optional<string> Name { get; set; }
[JsonProperty("errors")]
public Error[] Errors { get; set; }
}
}

+ 2
- 2
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -2235,7 +2235,7 @@ namespace Discord.API
if (x.HttpCode == HttpStatusCode.BadRequest)
{
var json = (x.Request as JsonRestRequest).Json;
throw new ApplicationCommandException(json, x);
throw new ApplicationCommandException(x);
}
}

@@ -2249,7 +2249,7 @@ namespace Discord.API
if (x.HttpCode == HttpStatusCode.BadRequest)
{
var json = (x.Request as JsonRestRequest).Json;
throw new ApplicationCommandException(json, x);
throw new ApplicationCommandException(x);
}

throw;


+ 5
- 3
src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs View File

@@ -12,8 +12,8 @@ namespace Discord.Net.Converters
{
#region DiscordContractResolver
private static readonly TypeInfo _ienumerable = typeof(IEnumerable<ulong[]>).GetTypeInfo();
private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize");
private static readonly MethodInfo _shouldSerialize = typeof(DiscordContractResolver).GetTypeInfo().GetDeclaredMethod("ShouldSerialize");
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
@@ -58,7 +58,7 @@ namespace Discord.Net.Converters
else if (genericType == typeof(EntityOrId<>))
return MakeGenericConverter(property, propInfo, typeof(UInt64EntityOrIdConverter<>), type.GenericTypeArguments[0], depth);
}
#endregion
#endregion

#region Primitives
bool hasInt53 = propInfo.GetCustomAttribute<Int53Attribute>() != null;
@@ -87,6 +87,8 @@ namespace Discord.Net.Converters
return MessageComponentConverter.Instance;
if (type == typeof(API.Interaction))
return InteractionConverter.Instance;
if (type == typeof(API.DiscordError))
return DiscordErrorConverter.Instance;
if (type == typeof(GuildFeatures))
return GuildFeaturesConverter.Instance;



+ 88
- 0
src/Discord.Net.Rest/Net/Converters/DiscordErrorConverter.cs View File

@@ -0,0 +1,88 @@
using Discord.API;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Net.Converters
{
internal class DiscordErrorConverter : JsonConverter
{
public static DiscordErrorConverter Instance
=> new DiscordErrorConverter();

public override bool CanConvert(Type objectType) => objectType == typeof(DiscordError);

public override bool CanRead => true;
public override bool CanWrite => false;

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
var err = new API.DiscordError();


var result = obj.GetValue("errors", StringComparison.OrdinalIgnoreCase);
result?.Parent.Remove();

// Populate the remaining properties.
using (var subReader = obj.CreateReader())
{
serializer.Populate(subReader, err);
}

if (result != null)
{
var innerReader = result.CreateReader();

var errors = ReadErrors(innerReader);
err.Errors = errors.ToArray();
}

return err;
}

private List<ErrorDetails> ReadErrors(JsonReader reader, string path = "")
{
List<ErrorDetails> errs = new List<ErrorDetails>();
var obj = JObject.Load(reader);
var props = obj.Properties();
foreach (var prop in props)
{
if (prop.Name == "_errors" && path == "") // root level error
{
errs.Add(new ErrorDetails()
{
Name = Optional<string>.Unspecified,
Errors = prop.Value.ToObject<Error[]>()
});
}
else if (prop.Name == "_errors") // path errors (not root level)
{
errs.Add(new ErrorDetails()
{
Name = path,
Errors = prop.Value.ToObject<Error[]>()
});
}
else if(int.TryParse(prop.Name, out var i)) // array value
{
var r = prop.Value.CreateReader();
errs.AddRange(ReadErrors(r, path + $"[{i}]"));
}
else // property name
{
var r = prop.Value.CreateReader();
errs.AddRange(ReadErrors(r, path + $"{(path != "" ? "." : "")}{prop.Name[0].ToString().ToUpper() + new string(prop.Name.Skip(1).ToArray())}"));
}
}

return errs;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}
}

+ 7
- 8
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -1,3 +1,4 @@
using Discord.API;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
@@ -5,6 +6,7 @@ using System;
using System.Diagnostics;
#endif
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
@@ -99,9 +101,7 @@ namespace Discord.Net.Queue

continue; //Retry
default:
int? code = null;
string reason = null;
object errors = null;
API.DiscordError error = null;
if (response.Stream != null)
{
try
@@ -109,15 +109,14 @@ namespace Discord.Net.Queue
using (var reader = new StreamReader(response.Stream))
using (var jsonReader = new JsonTextReader(reader))
{
var json = JToken.Load(jsonReader);
try { code = json.Value<int>("code"); } catch { };
try { reason = json.Value<string>("message"); } catch { };
try { errors = json.Value<object>("errors"); } catch { };
error = Discord.Rest.DiscordRestClient.Serializer.Deserialize<API.DiscordError>(jsonReader);
}
}
catch { }
}
throw new HttpException(response.StatusCode, request, code, reason, errors);
throw new HttpException(response.StatusCode, request, error?.Code, error.Message, error.Errors.IsSpecified
? error.Errors.Value.Select(x => new DiscordJsonError(x.Name.GetValueOrDefault("root"), x.Errors.Select(y => new DiscordError(y.Code, y.Message)).ToArray())).ToArray()
: null);
}
}
else


+ 63
- 0
src/Discord.Net.WebSocket/API/Voice/VoiceCloseCode.cs View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API.Voice
{
/// <summary>
/// Represents generic op codes for voice disconnect.
/// </summary>
public enum VoiceCloseCode
{
/// <summary>
/// You sent an invalid opcode.
/// </summary>
UnknownOpcode = 4001,
/// <summary>
/// You sent an invalid payload in your identifying to the Gateway.
/// </summary>
DecodeFailure = 4002,
/// <summary>
/// You sent a payload before identifying with the Gateway.
/// </summary>
NotAuthenticated = 4003,
/// <summary>
/// The token you sent in your identify payload is incorrect.
/// </summary>
AuthenticationFailed = 4004,
/// <summary>
/// You sent more than one identify payload. Stahp.
/// </summary>
AlreadyAuthenticated = 4005,
/// <summary>
/// Your session is no longer valid.
/// </summary>
SessionNolongerValid = 4006,
/// <summary>
/// Your session has timed out.
/// </summary>
SessionTimeout = 4009,
/// <summary>
/// We can't find the server you're trying to connect to.
/// </summary>
ServerNotFound = 4011,
/// <summary>
/// We didn't recognize the protocol you sent.
/// </summary>
UnknownProtocol = 4012,
/// <summary>
/// Channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped. Should not reconnect.
/// </summary>
Disconnected = 4014,
/// <summary>
/// The server crashed. Our bad! Try resuming.
/// </summary>
VoiceServerCrashed = 4015,
/// <summary>
/// We didn't recognize your encryption.
/// </summary>
UnknownEncryptionMode = 4016,
}
}

Loading…
Cancel
Save