diff --git a/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs b/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs index c88e49f..5b10bd2 100644 --- a/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs +++ b/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs @@ -2,11 +2,19 @@ namespace GraphQLSharp; public class GraphQLHttpException : GraphQLException { + public IReadOnlyList Errors { get; } + public GraphQLHttpException(GraphQLRequest request, HttpResponse httpResponse, Exception ex) - : base(request, httpResponse, $""" - GraphQL request failed with an HTTP error: - Request: {request} + : this(new GraphQLHttpError { Request = request, HttpResponse = httpResponse }, ex) + { + } + + public GraphQLHttpException(GraphQLHttpError error, Exception ex) + : base(error.Request, error.HttpResponse, $""" + GraphQL request failed with an HTTP error: {(error.errors != null ? string.Join("\n", error.errors) : "")} + Request: {error.Request} """, ex) { + Errors = Array.AsReadOnly(error.errors?.ToArray() ?? Array.Empty()); } -} \ No newline at end of file +} diff --git a/GraphQLSharp/Client/GraphQLClient.cs b/GraphQLSharp/Client/GraphQLClient.cs index e986605..575aced 100644 --- a/GraphQLSharp/Client/GraphQLClient.cs +++ b/GraphQLSharp/Client/GraphQLClient.cs @@ -102,7 +102,20 @@ private async Task> ExecuteCoreAsync(TRequest request, Can } catch (Exception httpEx) { - throw new GraphQLHttpException(request, httpResponse, httpEx); + GraphQLHttpError gqlHttpError = null; + try + { + gqlHttpError = await httpResponseMsg.Content.ReadFromJsonAsync(_options.JsonSerializerOptions ?? Serializer.GetOptions(), + cancellationToken); + } + catch (Exception errorDetailsException) when (errorDetailsException is JsonException or InvalidOperationException) + { + // Error details are optional; preserve the HTTP failure for callers and retries. + } + gqlHttpError ??= new GraphQLHttpError(); + gqlHttpError.Request = request; + gqlHttpError.HttpResponse = httpResponse; + throw new GraphQLHttpException(gqlHttpError, httpEx); } GraphQLResponse res; @@ -148,4 +161,4 @@ private HttpRequestMessage CreateHttpRequest(TRequest request, HttpClient httpCl _options.ConfigureHttpRequestHeaders?.Invoke(requestMessage.Headers); return requestMessage; } -} \ No newline at end of file +} diff --git a/GraphQLSharp/Client/Response/GraphQLHttpError.cs b/GraphQLSharp/Client/Response/GraphQLHttpError.cs new file mode 100644 index 0000000..49202d7 --- /dev/null +++ b/GraphQLSharp/Client/Response/GraphQLHttpError.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace GraphQLSharp; + +public class GraphQLHttpError : IGraphQLObject +{ + [JsonIgnore] + public GraphQLRequest Request { get; internal set; } + + [JsonIgnore] + public HttpResponse HttpResponse { get; internal set; } + + [JsonConverter(typeof(GraphQLHttpErrorMessageConverter))] + public List errors { get; set; } +} diff --git a/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs new file mode 100644 index 0000000..cf8257b --- /dev/null +++ b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GraphQLSharp; + +// Normalizes the value of an HTTP error response's "errors" property into individual messages: +// "Service unavailable" -> one message +// ["First error", "Second error"] -> multiple messages +// [{"message":"First"}, {"message":"Second"}] -> GraphQL error object messages +// Arrays may mix strings and error objects; message order is preserved. +// JSON null is handled by System.Text.Json and remains null. +internal sealed class GraphQLHttpErrorMessageConverter : JsonConverter> +{ + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + return [reader.GetString()]; + + using var document = JsonDocument.ParseValue(ref reader); + var errors = document.RootElement; + if (errors.ValueKind != JsonValueKind.Array) + return null; + + var messages = new List(); + foreach (var error in errors.EnumerateArray()) + { + if (error.ValueKind == JsonValueKind.String) + messages.Add(error.GetString()); + else if (error.ValueKind == JsonValueKind.Object + && error.TryGetProperty("message", out var message) + && message.ValueKind == JsonValueKind.String) + messages.Add(message.GetString()); + } + return messages; + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + // Non-null lists are always written as JSON string arrays, regardless of the input format. + writer.WriteStartArray(); + foreach (var message in value) + writer.WriteStringValue(message); + writer.WriteEndArray(); + } +}