From 6cc38a95903ff9d927440bb9ba17d473414db8d2 Mon Sep 17 00:00:00 2001 From: matnun-br Date: Thu, 5 Nov 2026 12:08:31 +1100 Subject: [PATCH 1/6] Add error message when available to GraphQLHttpException --- .../Client/Exceptions/GraphQLHttpException.cs | 8 ++++---- GraphQLSharp/Client/GraphQLClient.cs | 7 ++++++- GraphQLSharp/Client/Response/GraphQLHttpError.cs | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 GraphQLSharp/Client/Response/GraphQLHttpError.cs diff --git a/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs b/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs index c88e49f..edd8408 100644 --- a/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs +++ b/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs @@ -2,10 +2,10 @@ namespace GraphQLSharp; public class GraphQLHttpException : GraphQLException { - public GraphQLHttpException(GraphQLRequest request, HttpResponse httpResponse, Exception ex) - : base(request, httpResponse, $""" - GraphQL request failed with an HTTP error: - Request: {request} + public GraphQLHttpException(GraphQLHttpError error, Exception ex) + : base(error.Request, error.HttpResponse, $""" + GraphQL request failed with an HTTP error: {(error.errors != null ? $"{error.errors}" : "")} + Request: {error.Request} """, ex) { } diff --git a/GraphQLSharp/Client/GraphQLClient.cs b/GraphQLSharp/Client/GraphQLClient.cs index e986605..3a6ded1 100644 --- a/GraphQLSharp/Client/GraphQLClient.cs +++ b/GraphQLSharp/Client/GraphQLClient.cs @@ -102,7 +102,12 @@ private async Task> ExecuteCoreAsync(TRequest request, Can } catch (Exception httpEx) { - throw new GraphQLHttpException(request, httpResponse, httpEx); + var gqlHttpError = await httpResponseMsg.Content.ReadFromJsonAsync(cancellationToken); + if (gqlHttpError == null) + gqlHttpError = new GraphQLHttpError(); + gqlHttpError.Request = request; + gqlHttpError.HttpResponse = httpResponse; + throw new GraphQLHttpException(gqlHttpError, httpEx); } GraphQLResponse res; diff --git a/GraphQLSharp/Client/Response/GraphQLHttpError.cs b/GraphQLSharp/Client/Response/GraphQLHttpError.cs new file mode 100644 index 0000000..2bf951e --- /dev/null +++ b/GraphQLSharp/Client/Response/GraphQLHttpError.cs @@ -0,0 +1,14 @@ +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; } + + public string errors { get; set; } +} From ba1c98a226b896fe9d979e998147845260121319 Mon Sep 17 00:00:00 2001 From: matnun-br Date: Thu, 5 Nov 2026 12:27:57 +1100 Subject: [PATCH 2/6] Add GraphQLHttpErrorMessageConverter for error message handling --- GraphQLSharp/Client/GraphQLClient.cs | 15 +++++++--- .../Client/Response/GraphQLHttpError.cs | 1 + .../GraphQLHttpErrorMessageConverter.cs | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs diff --git a/GraphQLSharp/Client/GraphQLClient.cs b/GraphQLSharp/Client/GraphQLClient.cs index 3a6ded1..e319464 100644 --- a/GraphQLSharp/Client/GraphQLClient.cs +++ b/GraphQLSharp/Client/GraphQLClient.cs @@ -102,9 +102,16 @@ private async Task> ExecuteCoreAsync(TRequest request, Can } catch (Exception httpEx) { - var gqlHttpError = await httpResponseMsg.Content.ReadFromJsonAsync(cancellationToken); - if (gqlHttpError == null) - gqlHttpError = new GraphQLHttpError(); + GraphQLHttpError gqlHttpError = null; + try + { + gqlHttpError = await httpResponseMsg.Content.ReadFromJsonAsync(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); @@ -153,4 +160,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 index 2bf951e..ffa542d 100644 --- a/GraphQLSharp/Client/Response/GraphQLHttpError.cs +++ b/GraphQLSharp/Client/Response/GraphQLHttpError.cs @@ -10,5 +10,6 @@ public class GraphQLHttpError : IGraphQLObject [JsonIgnore] public HttpResponse HttpResponse { get; internal set; } + [JsonConverter(typeof(GraphQLHttpErrorMessageConverter))] public string errors { get; set; } } diff --git a/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs new file mode 100644 index 0000000..adbe35e --- /dev/null +++ b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GraphQLSharp; + +internal sealed class GraphQLHttpErrorMessageConverter : JsonConverter +{ + public override string 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; + + return string.Join("\n", errors.EnumerateArray() + .Where(error => error.ValueKind == JsonValueKind.Object + && error.TryGetProperty("message", out var message) + && message.ValueKind == JsonValueKind.String) + .Select(error => error.GetProperty("message").GetString())); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } +} From b584ac328e86871184c0996027560363d5dbc189 Mon Sep 17 00:00:00 2001 From: matnun-br Date: Thu, 5 Nov 2026 12:36:07 +1100 Subject: [PATCH 3/6] Refactor GraphQLHttpError and GraphQLHttpErrorMessageConverter to handle error messages as a list --- .../Client/Exceptions/GraphQLHttpException.cs | 4 +-- .../Client/Response/GraphQLHttpError.cs | 2 +- .../GraphQLHttpErrorMessageConverter.cs | 25 +++++++++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs b/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs index edd8408..8db508d 100644 --- a/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs +++ b/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs @@ -4,9 +4,9 @@ public class GraphQLHttpException : GraphQLException { public GraphQLHttpException(GraphQLHttpError error, Exception ex) : base(error.Request, error.HttpResponse, $""" - GraphQL request failed with an HTTP error: {(error.errors != null ? $"{error.errors}" : "")} + GraphQL request failed with an HTTP error: {(error.errors != null ? string.Join("\n", error.errors) : "")} Request: {error.Request} """, ex) { } -} \ No newline at end of file +} diff --git a/GraphQLSharp/Client/Response/GraphQLHttpError.cs b/GraphQLSharp/Client/Response/GraphQLHttpError.cs index ffa542d..49202d7 100644 --- a/GraphQLSharp/Client/Response/GraphQLHttpError.cs +++ b/GraphQLSharp/Client/Response/GraphQLHttpError.cs @@ -11,5 +11,5 @@ public class GraphQLHttpError : IGraphQLObject public HttpResponse HttpResponse { get; internal set; } [JsonConverter(typeof(GraphQLHttpErrorMessageConverter))] - public string errors { get; set; } + public List errors { get; set; } } diff --git a/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs index adbe35e..f05e367 100644 --- a/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs +++ b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs @@ -3,27 +3,36 @@ namespace GraphQLSharp; -internal sealed class GraphQLHttpErrorMessageConverter : JsonConverter +internal sealed class GraphQLHttpErrorMessageConverter : JsonConverter> { - public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) - return reader.GetString(); + return [reader.GetString()]; using var document = JsonDocument.ParseValue(ref reader); var errors = document.RootElement; if (errors.ValueKind != JsonValueKind.Array) return null; - return string.Join("\n", errors.EnumerateArray() - .Where(error => error.ValueKind == JsonValueKind.Object + 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) - .Select(error => error.GetProperty("message").GetString())); + messages.Add(message.GetString()); + } + return messages; } - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) { - writer.WriteStringValue(value); + writer.WriteStartArray(); + foreach (var message in value) + writer.WriteStringValue(message); + writer.WriteEndArray(); } } From ca7b7f116e46ea787249d95e6c5ed5e8c1668d8b Mon Sep 17 00:00:00 2001 From: matnun-br Date: Thu, 5 Nov 2026 12:50:33 +1100 Subject: [PATCH 4/6] Add Errors property to GraphQLHttpException for improved error handling --- .../GraphQLHttpExceptionTests.cs | 28 +++++++++++++++++++ .../Client/Exceptions/GraphQLHttpException.cs | 8 ++++++ GraphQLSharp/Client/GraphQLClient.cs | 3 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs diff --git a/GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs b/GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs new file mode 100644 index 0000000..761c14c --- /dev/null +++ b/GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs @@ -0,0 +1,28 @@ +using System.Net; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GraphQLSharp.Tests; + +[TestClass] +public class GraphQLHttpExceptionTests +{ + [TestMethod] + public void OriginalPublicConstructorPreservesMetadata() + { + var constructor = typeof(GraphQLHttpException).GetConstructor( + new[] { typeof(GraphQLRequest), typeof(HttpResponse), typeof(Exception) }); + Assert.IsNotNull(constructor, "Retain the original public constructor for existing consumers."); + GraphQLRequest request = "{ shop { id } }"; + using var responseMessage = new HttpResponseMessage(HttpStatusCode.BadGateway); + var response = new HttpResponse(responseMessage); + var innerException = new HttpRequestException("Bad gateway"); + + var exception = (GraphQLHttpException)constructor.Invoke(new object[] { request, response, innerException }); + + Assert.AreSame(request, exception.Request); + Assert.AreSame(response, exception.HttpResponse); + Assert.AreSame(innerException, exception.InnerException); + Assert.AreEqual(0, exception.Errors.Count); + StringAssert.Contains(exception.Message, request.ToString()); + } +} diff --git a/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs b/GraphQLSharp/Client/Exceptions/GraphQLHttpException.cs index 8db508d..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) + : 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()); } } diff --git a/GraphQLSharp/Client/GraphQLClient.cs b/GraphQLSharp/Client/GraphQLClient.cs index e319464..575aced 100644 --- a/GraphQLSharp/Client/GraphQLClient.cs +++ b/GraphQLSharp/Client/GraphQLClient.cs @@ -105,7 +105,8 @@ private async Task> ExecuteCoreAsync(TRequest request, Can GraphQLHttpError gqlHttpError = null; try { - gqlHttpError = await httpResponseMsg.Content.ReadFromJsonAsync(cancellationToken); + gqlHttpError = await httpResponseMsg.Content.ReadFromJsonAsync(_options.JsonSerializerOptions ?? Serializer.GetOptions(), + cancellationToken); } catch (Exception errorDetailsException) when (errorDetailsException is JsonException or InvalidOperationException) { From b1a87aa73229503f49cdb8d2435c3d6701a2ce04 Mon Sep 17 00:00:00 2001 From: matnun-br Date: Thu, 5 Nov 2026 12:52:30 +1100 Subject: [PATCH 5/6] Remove tests --- .../GraphQLHttpExceptionTests.cs | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs diff --git a/GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs b/GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs deleted file mode 100644 index 761c14c..0000000 --- a/GraphQLSharp.Tests/GraphQLHttpExceptionTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Net; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace GraphQLSharp.Tests; - -[TestClass] -public class GraphQLHttpExceptionTests -{ - [TestMethod] - public void OriginalPublicConstructorPreservesMetadata() - { - var constructor = typeof(GraphQLHttpException).GetConstructor( - new[] { typeof(GraphQLRequest), typeof(HttpResponse), typeof(Exception) }); - Assert.IsNotNull(constructor, "Retain the original public constructor for existing consumers."); - GraphQLRequest request = "{ shop { id } }"; - using var responseMessage = new HttpResponseMessage(HttpStatusCode.BadGateway); - var response = new HttpResponse(responseMessage); - var innerException = new HttpRequestException("Bad gateway"); - - var exception = (GraphQLHttpException)constructor.Invoke(new object[] { request, response, innerException }); - - Assert.AreSame(request, exception.Request); - Assert.AreSame(response, exception.HttpResponse); - Assert.AreSame(innerException, exception.InnerException); - Assert.AreEqual(0, exception.Errors.Count); - StringAssert.Contains(exception.Message, request.ToString()); - } -} From 804efd2f9ea5b8e5766daa6049edbad8e406907e Mon Sep 17 00:00:00 2001 From: matnun-br Date: Thu, 5 Nov 2026 12:53:33 +1100 Subject: [PATCH 6/6] Add comments --- .../Serialization/GraphQLHttpErrorMessageConverter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs index f05e367..cf8257b 100644 --- a/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs +++ b/GraphQLSharp/Serialization/GraphQLHttpErrorMessageConverter.cs @@ -3,6 +3,12 @@ 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) @@ -30,6 +36,7 @@ public override List Read(ref Utf8JsonReader reader, Type typeToConvert, 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);