Skip to content

Fix JSON parsing crash on non-JSON API error responses#18

Merged
fostimus merged 22 commits into
mainfrom
copilot/fix-api-error-handling
Mar 3, 2026
Merged

Fix JSON parsing crash on non-JSON API error responses#18
fostimus merged 22 commits into
mainfrom
copilot/fix-api-error-handling

Conversation

Copilot AI commented Feb 21, 2026

Copy link
Copy Markdown
Contributor

dotnet-anvil Error Handling Changes

The Problem

When the Anvil API returned a non-JSON error response — most commonly a 429 rate limit with a plain-text body — the client crashed with a JsonReaderException instead of surfacing the actual API error. Both the REST and GraphQL clients called JsonConvert.DeserializeObject on the response body without checking whether it was JSON first. This meant consumers couldn't catch or handle rate limits, gateway errors, or any other response that wasn't well-formed JSON.

What Changed

All API errors — REST and GraphQL — now throw AnvilClientException with structured error metadata. Non-JSON responses are handled gracefully: the raw response body, HTTP status code, and response headers are all available on the exception. Consumers no longer need to worry about the client crashing before they can inspect the error.

0.6.0

Breaking Changes

GraphQL errors now throw AnvilClientException

Previously, HTTP-level errors from the GraphQL client threw GraphQLHttpRequestException. They now throw AnvilClientException with the original preserved as InnerException.

Before:

try
{
    var result = await graphqlClient.SendQuery(query, variables);
}
catch (GraphQLHttpRequestException ex)
{
    // handle error
}

After:

try
{
    var result = await graphqlClient.SendQuery(query, variables);
}
catch (AnvilClientException ex)
{
    Console.WriteLine(ex.HttpStatusCode);    // e.g. 429
    Console.WriteLine(ex.ResponseContent);   // raw response body
    Console.WriteLine(ex.ResponseHeaders);   // dictionary of headers
    Console.WriteLine(ex.InnerException);    // original GraphQLHttpRequestException
}

SendGetRequest now throws on error responses

Previously, SendGetRequest silently returned failed HttpResponseMessage objects without throwing. It now throws AnvilClientException, consistent with SendPostRequest. This affects DownloadDocuments and any direct calls to SendGetRequest.

Before:

var response = await restClient.SendGetRequest(uri);
if (!response.IsSuccessStatusCode) { /* manual check */ }

After:

try
{
    var response = await restClient.SendGetRequest(uri);
}
catch (AnvilClientException ex)
{
    // error details available on ex
}

GraphQL error message format changed

Multiple GraphQL errors are now joined with "; " instead of concatenated directly.

Before: "Field required""Email invalid"
After: "Field required; Email invalid"

Individual messages are also available via ex.Data["Message1"], ex.Data["Message2"], etc.

Target framework simplified

The library now targets netstandard2.0 only, replacing net5.0;netstandard2.0;net461. Since netstandard2.0 is compatible with .NET Framework 4.6.1+, .NET Core 2+, and all modern .NET versions, most consumers are unaffected. Consumers that pinned to net5.0 or net461 should verify compatibility.

New Features

AnvilClientException properties

All error paths now populate these properties:

Property Type Description
HttpStatusCode HttpStatusCode? The HTTP status code (e.g. 429, 401, 500)
ResponseHeaders Dictionary<string, IEnumerable<string>>? Response headers (case-insensitive lookup)
ResponseContent string? Raw response body
InnerException Exception? Original exception (GraphQL path only)
Data["Message1"], etc. object? Individual error messages from the API

IDisposable support

RestClient and GraphQLClient now implement IDisposable to clean up their underlying HttpClient. Existing code that doesn't call Dispose() will continue to work.

using var client = new RestClient("your-api-key");

Bug Fixes

  • Non-JSON error responses no longer crash the client. Both REST and GraphQL paths now catch JsonException and fall back gracefully, preserving the raw response content on the exception.
  • RestClient.CreateExceptionFromResponse is now async. The previous implementation used .Result to block on ReadAsStringAsync(), which could deadlock in synchronous contexts or UI threads.
  • EtchSigner.SignerType no longer throws NullReferenceException on null input.
  • DocumentMarkup.Markup and HtmlCssMarkup.Html nullable annotations fixed. These properties were annotated as non-nullable but could be null after JSON deserialization.
  • Error messages with null message fields are now skipped. Previously, JSON error entries without a message key would insert null values into the exception's Data dictionary.

Dependencies

  • Upgraded Newtonsoft.Json from 12.0.3 to 13.0.3 to resolve a high-severity vulnerability (GHSA-5crp-9r3c-p9vr).

Internal

  • Added GitHub Actions CI workflow (build, format check, tests on PRs to main).
  • Test project updated to target net10.0.
  • InternalsVisibleTo added to support unit testing of internal error-wrapping logic.
  • WrapGraphQLException extracted as an internal static method from SendQuery<T> to make GraphQL error-wrapping logic unit-testable without mocking HTTP calls.
  • ResponseHeaders dictionaries use StringComparer.OrdinalIgnoreCase in both REST and GraphQL paths, since HTTP headers are case-insensitive.
  • Fixes Client crashes on api error responses and throws JSON parsing error #17

Copilot AI and others added 4 commits February 21, 2026 04:29
…d ResponseContent properties

Co-authored-by: fostimus <8053880+fostimus@users.noreply.github.com>
…prehensive tests

Co-authored-by: fostimus <8053880+fostimus@users.noreply.github.com>
…n-JSON API responses

Co-authored-by: fostimus <8053880+fostimus@users.noreply.github.com>
Co-authored-by: fostimus <8053880+fostimus@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix client crash on API error responses Fix JSON parsing crash on non-JSON API error responses Feb 21, 2026
Copilot AI requested a review from fostimus February 21, 2026 04:39
@fostimus fostimus marked this pull request as ready for review February 22, 2026 07:39
@fostimus fostimus merged commit 8d72dd9 into main Mar 3, 2026
1 check passed
@fostimus fostimus deleted the copilot/fix-api-error-handling branch March 3, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client crashes on api error responses and throws JSON parsing error

3 participants