Skip to content

[Fusion] Forward subgraph errors #8521

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ namespace HotChocolate.Transport.Http;
/// </summary>
public sealed class GraphQLHttpResponse : IDisposable
{
private static readonly OperationResult s_transportError = CreateTransportError();

private readonly HttpResponseMessage _message;

/// <summary>
Expand Down Expand Up @@ -182,21 +180,16 @@ public IAsyncEnumerable<OperationResult> ReadAsResultStreamAsync()
ct => ReadAsResultInternalAsync(contentType.CharSet, ct));
}

return SingleResult(new ValueTask<OperationResult>(s_transportError));
_message.EnsureSuccessStatusCode();

throw new InvalidOperationException("Received a successful response with an unexpected content type.");
}

private static async IAsyncEnumerable<OperationResult> SingleResult(ValueTask<OperationResult> result)
{
yield return await result.ConfigureAwait(false);
}

private static OperationResult CreateTransportError()
=> new OperationResult(
errors: JsonDocument.Parse(
"""
[{"message": "Internal Execution Error"}]
""").RootElement);

/// <summary>
/// Disposes the underlying <see cref="HttpResponseMessage"/>.
/// </summary>
Expand Down
56 changes: 56 additions & 0 deletions src/HotChocolate/Core/src/Execution.Abstractions/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,62 @@ public Path Append(int index)
return new IndexerPathSegment(this, index);
}

/// <summary>
/// Appends another path to this path.
/// </summary>
/// <param name="path">
/// The other path.
/// </param>
/// <returns>
/// the combined path.
/// </returns>
public Path Append(Path path)
{
ArgumentNullException.ThrowIfNull(path);

if (path.IsRoot)
{
return this;
}

var stack = new Stack<object>();
var current = path;

while (!current.IsRoot)
{
switch (current)
{
case IndexerPathSegment indexer:
stack.Push(indexer.Index);
break;

case NamePathSegment name:
stack.Push(name.Name);
break;

default:
throw new NotSupportedException("Unsupported path segment type.");
}

current = current.Parent;
}

var newPath = this;

while (stack.Count > 0)
{
var segment = stack.Pop();
newPath = segment switch
{
string name => newPath.Append(name),
int index => newPath.Append(index),
_ => throw new NotSupportedException("Unsupported path segment type.")
};
}

return newPath;
}

/// <summary>
/// Generates a string that represents the current path.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace HotChocolate.Fusion.Execution.Clients;

public sealed class ErrorTrie : Dictionary<object, ErrorTrie>
{
public IError? Error { get; set; }

public (object[] Path, IError Error)? FindPathToFirstError()
{
if (Error is not null)
{
return ([], Error);
}

var stack = new Stack<(ErrorTrie Node, List<object> Path)>();

foreach (var kvp in this)
{
stack.Push((kvp.Value, [kvp.Key]));
}

while (stack.Count > 0)
{
var (node, path) = stack.Pop();

if (node.Error is not null)
{
return ([..path], node.Error);
}

foreach (var kvp in node)
{
stack.Push((kvp.Value, [..path, kvp.Key]));
}
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace HotChocolate.Fusion.Execution.Clients;

public sealed class SourceSchemaError(IError error, Path path)
{
public IError Error { get; } = error;

public Path Path { get; } = path;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Text.Json;

namespace HotChocolate.Fusion.Execution.Clients;

public sealed class SourceSchemaErrors
{
/// <summary>
/// Errors without a path.
/// </summary>
public required List<IError>? RootErrors { get; init; }

public required ErrorTrie Trie { get; init; }

public static SourceSchemaErrors? From(JsonElement json)
{
if (json.ValueKind != JsonValueKind.Array)
{
return null;
}

List<IError>? rootErrors = null;
ErrorTrie root = new ErrorTrie();

foreach (var jsonError in json.EnumerateArray())
{
var currentTrie = root;

var error = CreateError(jsonError);

if (error is null)
{
continue;
}

if (error.Path is null)
{
rootErrors ??= [];
rootErrors.Add(error);
continue;
}

var pathSegments = error.Path.ToList();
var lastPathIndex = pathSegments.Count - 1;

for (var i = 0; i < pathSegments.Count; i++)
{
var pathSegment = pathSegments[i];

if (currentTrie.TryGetValue(pathSegment, out var trieAtPath))
{
currentTrie = trieAtPath;
}
else
{
var newTrie = new ErrorTrie();
currentTrie[pathSegment] = newTrie;
currentTrie = newTrie;
}

if (i == lastPathIndex)
{
currentTrie.Error = error;
}
}
}

return new SourceSchemaErrors { RootErrors = rootErrors, Trie = root };
}

private static IError? CreateError(JsonElement jsonError)
{
if (jsonError.ValueKind is not JsonValueKind.Object)
{
return null;
}

if (jsonError.TryGetProperty("message", out var message)
&& message.ValueKind is JsonValueKind.String)
{
var errorBuilder = ErrorBuilder.New()
.SetMessage(message.GetString()!);

if (jsonError.TryGetProperty("path", out var path) && path.ValueKind == JsonValueKind.Array)
{
errorBuilder.SetPath(CreatePathFromJson(path));
}

if (jsonError.TryGetProperty("code", out var code)
&& code.ValueKind is JsonValueKind.String)
{
errorBuilder.SetCode(code.GetString());
}

if (jsonError.TryGetProperty("extensions", out var extensions)
&& extensions.ValueKind is JsonValueKind.Object)
{
foreach (var property in extensions.EnumerateObject())
{
errorBuilder.SetExtension(property.Name, property.Value);
}
}

return errorBuilder.Build();
}

return null;
}

private static Path CreatePathFromJson(JsonElement errorSubPath)
{
var path = Path.Root;

for (var i = 0; i < errorSubPath.GetArrayLength(); i++)
{
path = errorSubPath[i] switch
{
{ ValueKind: JsonValueKind.String } nameElement => path.Append(nameElement.GetString()!),
{ ValueKind: JsonValueKind.Number } indexElement => path.Append(indexElement.GetInt32()),
_ => throw new InvalidOperationException("The error path contains an unsupported element."),
};
}

return path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,6 @@ public sealed class SourceSchemaResult : IDisposable
{
private readonly IDisposable? _resource;

public SourceSchemaResult(Path path, JsonDocument document, FinalMessage final = FinalMessage.Undefined)
{
ArgumentNullException.ThrowIfNull(path);
ArgumentNullException.ThrowIfNull(document);

_resource = document;
Path = path;

var root = document.RootElement;
if (root.ValueKind != JsonValueKind.Object)
{
return;
}

if (root.TryGetProperty("data", out var data))
{
Data = data;
}

if (root.TryGetProperty("errors", out var errors))
{
Errors = errors;
}

if (root.TryGetProperty("extensions", out var extensions))
{
Extensions = extensions;
}

Final = final;
}

public SourceSchemaResult(
Path path,
IDisposable resource,
Expand All @@ -52,7 +20,7 @@ public SourceSchemaResult(
_resource = resource;
Path = path;
Data = data;
Errors = errors;
Errors = SourceSchemaErrors.From(errors);
Extensions = extensions;
Final = final;
}
Expand All @@ -61,7 +29,7 @@ public SourceSchemaResult(

public JsonElement Data { get; }

public JsonElement Errors { get; }
public SourceSchemaErrors? Errors { get; }

public JsonElement Extensions { get; }

Expand Down
Loading