Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include the executed route in RestApiException #125

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions src/Disqord.Rest.Api/Default/DefaultRestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private async ValueTask<Stream> InternalExecuteAsync(IFormattedRoute route,
return responseStream;

if (statusCode > 499 && statusCode < 600)
throw new RestApiException(response.HttpResponse, response.HttpResponse.ReasonPhrase, null);
throw new RestApiException(route, response.HttpResponse, response.HttpResponse.ReasonPhrase, null);

RestApiErrorJsonModel? errorModel = null;
try
Expand All @@ -115,7 +115,7 @@ private async ValueTask<Stream> InternalExecuteAsync(IFormattedRoute route,
await responseStream.DisposeAsync().ConfigureAwait(false);
}

throw new RestApiException(response.HttpResponse, response.HttpResponse.ReasonPhrase, errorModel);
throw new RestApiException(route, response.HttpResponse, response.HttpResponse.ReasonPhrase, errorModel);
}
}
}
46 changes: 31 additions & 15 deletions src/Disqord.Rest.Api/RestApiException.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Disqord.Http;
using Disqord.Rest.Api;
using Disqord.Serialization.Json;
Expand All @@ -9,6 +10,16 @@ namespace Disqord.Rest;

public class RestApiException : Exception
{
/// <summary>
/// Gets the full API route that failed to execute.
/// </summary>
/// <remarks>
/// This route may include sensitive information such as webhook tokens and should not be displayed or exposed in public logs.
/// <para/>
/// <see cref="IFormattedRoute.BaseRoute"/> can be utilized instead for public-facing route logging.
/// </remarks>
public IFormattedRoute Route { get; }

/// <summary>
/// Gets the HTTP failure response.
/// </summary>
Expand Down Expand Up @@ -44,9 +55,10 @@ public bool IsServerError
}
}

public RestApiException(IHttpResponse httpResponse, string? reasonPhrase, RestApiErrorJsonModel? errorModel)
: base(GetErrorMessage(httpResponse.StatusCode, reasonPhrase, errorModel))
public RestApiException(IFormattedRoute route, IHttpResponse httpResponse, string? reasonPhrase, RestApiErrorJsonModel? errorModel)
: base(GetErrorMessage(route.BaseRoute, httpResponse.StatusCode, reasonPhrase, errorModel))
{
Route = route;
HttpResponse = httpResponse;
ReasonPhrase = reasonPhrase;
ErrorModel = errorModel;
Expand All @@ -65,14 +77,17 @@ public bool IsError(RestApiErrorCode code)
return errorModel != null && errorModel.Code == code;
}

private static string GetErrorMessage(HttpResponseStatusCode statusCode, string? reasonPhrase, RestApiErrorJsonModel? errorModel)
private static string GetErrorMessage(IFormattableRoute route, HttpResponseStatusCode statusCode, string? reasonPhrase, RestApiErrorJsonModel? errorModel)
{
var httpMessage = $"HTTP: {(Enum.IsDefined(statusCode) ? $"{(int) statusCode} {statusCode}" : statusCode)}.";
var messageBuilder = new StringBuilder($"Failed to execute route {route.Method}|{route.Path}.\n");

messageBuilder.Append($"HTTP: {(Enum.IsDefined(statusCode) ? $"{(int)statusCode} {statusCode}" : statusCode)}. ");

if (errorModel == null)
return $"{httpMessage} Reason phrase: {reasonPhrase}";
return messageBuilder.Append($"Reason phrase: {reasonPhrase}").ToString();

// HTTP: 400 BadRequest. Error message: Invalid Form Body
var message = $"{httpMessage} Error message: {errorModel.Message}";
messageBuilder.Append($"Error message: {errorModel.Message}");

// We check if Discord provided more detailed error messages.
if (errorModel.ExtensionData.TryGetValue("errors", out var errors) && errors is IJsonObject errorsObject)
Expand All @@ -83,7 +98,8 @@ private static string GetErrorMessage(HttpResponseStatusCode statusCode, string?
// We append the errors the message created above, example:
// embed.fields[0].name: "Must be 256 or fewer in length."
// embed.fields[1].value: "This field is required"
message += $"\n{string.Join('\n', extracted.Select(x => $"{x.Key}: {x.Value ?? "unknown error"}"))}";
messageBuilder.AppendJoin('\n', extracted.Select(x => $"{x.Key}: {x.Value}"));
//message += $"\n{string.Join('\n', extracted.Select(x => $"{x.Key}: {x.Value ?? "unknown error"}"))}";
}

/*
Expand All @@ -103,8 +119,6 @@ private static string GetErrorMessage(HttpResponseStatusCode statusCode, string?
*/
static IEnumerable<KeyValuePair<string, string>> ExtractErrors(IJsonObject jsonObject, string? key = null)
{
var extracted = new List<KeyValuePair<string, string>>();

// We enumerate the fields in the `errors` JSON object.
foreach (var (name, value) in jsonObject)
{
Expand All @@ -119,14 +133,18 @@ static IEnumerable<KeyValuePair<string, string>> ExtractErrors(IJsonObject jsonO
// If the value is not a JSON object, just ToString whatever it is.
if (value is not IJsonObject valueObject)
{
extracted.Add(KeyValuePair.Create(newKey, value?.ToString() ?? ""));
yield return KeyValuePair.Create(newKey, value?.ToString() ?? "");
continue;
}

// If the value has no `_errors` field it means there's more nested data, recurse.
if (!valueObject.TryGetValue("_errors", out var errors))
{
extracted.AddRange(ExtractErrors(valueObject, newKey));
foreach (var error in ExtractErrors(valueObject, newKey))
{
yield return error;
}

continue;
}

Expand All @@ -138,12 +156,10 @@ static IEnumerable<KeyValuePair<string, string>> ExtractErrors(IJsonObject jsonO
var messages = errorsArray.OfType<IJsonObject>()
.Select(static x => (x.GetValueOrDefault("message") ?? x.GetValueOrDefault("code"))?.ToString());

extracted.Add(KeyValuePair.Create(newKey, string.Join("; ", messages)));
yield return KeyValuePair.Create(newKey, string.Join("; ", messages));
}

return extracted;
}

return message;
return messageBuilder.ToString();
}
}
Loading