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

Improved error handling #1714

Merged
merged 1 commit into from
Jan 5, 2024
Merged
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
5 changes: 4 additions & 1 deletion samples/GraphQL.Samples.Http/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;

using Microsoft.AspNetCore.Mvc;

using Tanka.GraphQL.Executable;
using Tanka.GraphQL.Server;

Expand Down Expand Up @@ -59,7 +62,7 @@
app.Run();

// simple subscription generating numbers from 0 to the given number
static async IAsyncEnumerable<int> Count(int to, [EnumeratorCancellation] CancellationToken cancellationToken)
static async IAsyncEnumerable<int> Count(int to, [FromServices]ILogger<Program> logger, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var i = 0;
while (!cancellationToken.IsCancellationRequested)
Expand Down
76 changes: 40 additions & 36 deletions src/GraphQL.Server/WebSockets/ServerMethods.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Collections.Concurrent;

using Microsoft.AspNetCore.Http;

using Tanka.GraphQL.Fields;
using Tanka.GraphQL.Server.WebSockets.WebSocketPipe;
using Tanka.GraphQL.Validation;

Expand Down Expand Up @@ -56,40 +58,32 @@ await Channel.Complete(
private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscribeOrAborted)
{
var cancellationToken = unsubscribeOrAborted.Token;
var context = new GraphQLRequestContext
{
HttpContext = _httpContext,
RequestServices = _httpContext.RequestServices,
Request = new()
{
InitialValue = null,
Document = subscribe.Payload.Query,
OperationName = subscribe.Payload.OperationName,
Variables = subscribe.Payload.Variables
}
};

try
{
var context = new GraphQLRequestContext
{
HttpContext = _httpContext,
RequestServices = _httpContext.RequestServices,
Request = new()
{
InitialValue = null,
Document = subscribe.Payload.Query,
OperationName = subscribe.Payload.OperationName,
Variables = subscribe.Payload.Variables
}
};

await _requestDelegate(context);
await using var enumerator = context.Response.GetAsyncEnumerator(cancellationToken);

while (await enumerator.MoveNextAsync())
{
await Client.Next(new Next()
{
Id = subscribe.Id,
Payload = enumerator.Current
}, cancellationToken);
await Client.Next(new Next() { Id = subscribe.Id, Payload = enumerator.Current }, cancellationToken);
}

if (!cancellationToken.IsCancellationRequested)
{
await Client.Complete(new Complete()
{
Id = subscribe.Id
}, cancellationToken);
await Client.Complete(new Complete() { Id = subscribe.Id }, cancellationToken);
}
}
catch (OperationCanceledException)
Expand All @@ -99,26 +93,36 @@ await Client.Complete(new Complete()
catch (ValidationException x)
{
var validationResult = x.Result;
await Client.Error(new Error()
{
Id = subscribe.Id,
Payload = validationResult.Errors.Select(ve => ve.ToError()).ToArray()
}, cancellationToken);
await Client.Error(
new Error()
{
Id = subscribe.Id,
Payload = validationResult.Errors.Select(ve => ve.ToError()).ToArray()
}, cancellationToken);
}
catch (QueryException x)
{
await Client.Error(new Error()
{
Id = subscribe.Id,
Payload = new[]
await Client.Error(
new Error()
{
Id = subscribe.Id,
Payload = new[]
{
context.Errors?.FormatError(x)!
}
}, cancellationToken);
}
catch (Exception x)
{
await Client.Error(
new Error()
{
new ExecutionError()
Id = subscribe.Id,
Payload = new[]
{
Path = x.Path.Segments.ToArray(),
Message = x.Message
context.Errors?.FormatError(x)!
}
}
}, cancellationToken);
}, cancellationToken);
}
finally
{
Expand Down
5 changes: 5 additions & 0 deletions src/GraphQL/ErrorCollectorFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public IEnumerable<ExecutionError> GetErrors()
return _bag.Select(DefaultFormatError);
}

public ExecutionError FormatError(Exception x)
{
return DefaultFormatError(x);
}

public ExecutionError DefaultFormatError(Exception exception)
{
var rootCause = exception.GetBaseException();
Expand Down
3 changes: 1 addition & 2 deletions src/GraphQL/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Tanka.GraphQL.Fields;
using Tanka.GraphQL.Language.Nodes;
using Tanka.GraphQL.Language.Nodes;
using Tanka.GraphQL.Language.Nodes.TypeSystem;
using Tanka.GraphQL.ValueResolution;

Expand Down
26 changes: 25 additions & 1 deletion src/GraphQL/ExecutionError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class ExecutionError

[JsonPropertyName("locations")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<Location>? Locations { get; set; }
public List<SerializedLocation>? Locations { get; set; }

[JsonPropertyName("message")] public string Message { get; set; } = string.Empty;

Expand All @@ -23,4 +23,28 @@ public void Extend(string key, object value)

Extensions[key] = value;
}
}

public class SerializedLocation
{
public int Line { get; set; }

public int Column { get; set; }

public static implicit operator SerializedLocation(Location location)
{
return new()
{
Line = location.Line,
Column = location.Column
};
}
}

public static class ExecutionErrorExtensions
{
public static List<SerializedLocation> ToSerializedLocations(this IEnumerable<Location> locations)
{
return [.. locations];
}
}
108 changes: 84 additions & 24 deletions src/GraphQL/Executor.ExecuteSubscription.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
using System;
using System.Runtime.CompilerServices;

using Microsoft.VisualBasic.FileIO;

using Tanka.GraphQL.Features;
using Tanka.GraphQL.Language.Nodes;
Expand All @@ -15,11 +18,11 @@ public partial class Executor
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public static Task ExecuteSubscription(QueryContext context)
public static async Task ExecuteSubscription(QueryContext context)
{
context.RequestCancelled.ThrowIfCancellationRequested();

IAsyncEnumerable<object?> sourceStream = CreateSourceEventStream(
IAsyncEnumerable<object?> sourceStream = await CreateSourceEventStream(
context,
context.RequestCancelled);

Expand All @@ -29,7 +32,6 @@ public static Task ExecuteSubscription(QueryContext context)
context.RequestCancelled);

context.Response = responseStream;
return Task.CompletedTask;
}


Expand All @@ -40,9 +42,9 @@ public static Task ExecuteSubscription(QueryContext context)
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="QueryException"></exception>
public static async IAsyncEnumerable<object?> CreateSourceEventStream(
public static async Task<IAsyncEnumerable<object?>> CreateSourceEventStream(
QueryContext context,
[EnumeratorCancellation] CancellationToken cancellationToken)
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context.Schema.Subscription);

Expand All @@ -56,9 +58,9 @@ public static Task ExecuteSubscription(QueryContext context)
);

List<FieldSelection> fields = groupedFieldSet.Values.First();
Name fieldName = fields.First().Name;
FieldSelection fieldSelection = fields.First();

Name fieldName = fieldSelection.Name;

IReadOnlyDictionary<string, object?> coercedArgumentValues = ArgumentCoercion.CoerceArgumentValues(
context.Schema,
subscriptionType,
Expand All @@ -68,7 +70,7 @@ public static Task ExecuteSubscription(QueryContext context)
FieldDefinition? field = context.Schema.GetField(subscriptionType.Name, fieldName);

if (field is null)
yield break;
return AsyncEnumerableEx.Empty<object?>();

var path = new NodePath();
Subscriber? subscriber = context.Schema.GetSubscriber(subscriptionType.Name, fieldName);
Expand All @@ -92,13 +94,60 @@ public static Task ExecuteSubscription(QueryContext context)
QueryContext = context
};

await subscriber(resolverContext, cancellationToken);
try
{
await subscriber(resolverContext, cancellationToken);

if (resolverContext.ResolvedValue is null)
yield break;
if (resolverContext.ResolvedValue is null)
return AsyncEnumerableEx.Empty<object?>();
}
catch (Exception exception)
{
if (exception is not FieldException)
throw new FieldException(exception.Message, exception)
{
ObjectDefinition = subscriptionType,
Field = field,
Selection = fieldSelection,
Path = path
};

throw;
}

return Core(resolverContext, cancellationToken);

static async IAsyncEnumerable<object?> Core(SubscriberContext resolverContext, [EnumeratorCancellation]CancellationToken cancellationToken)
{
await using var e = resolverContext.ResolvedValue!.GetAsyncEnumerator(cancellationToken);

await foreach (object? evnt in resolverContext.ResolvedValue.WithCancellation(cancellationToken))
yield return evnt;
while (true)
{
try
{
if (!await e.MoveNextAsync())
{
yield break;
}

}
catch (Exception exception)
{
if (exception is not FieldException)
throw new FieldException(exception.Message, exception)
{
ObjectDefinition = resolverContext.ObjectDefinition,
Field = resolverContext.Field,
Selection = resolverContext.Selection,
Path = resolverContext.Path
};

throw;
}

yield return e.Current;
}
}
}

/// <summary>
Expand All @@ -109,25 +158,36 @@ public static Task ExecuteSubscription(QueryContext context)
/// <param name="sourceStream"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async IAsyncEnumerable<ExecutionResult> MapSourceToResponseEventStream(
public static IAsyncEnumerable<ExecutionResult> MapSourceToResponseEventStream(
QueryContext context,
IAsyncEnumerable<object?> sourceStream,
[EnumeratorCancellation] CancellationToken cancellationToken)
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context.Schema.Subscription);

ObjectDefinition? subscriptionType = context.Schema.Subscription;
SelectionSet selectionSet = context.OperationDefinition.SelectionSet;

await foreach (object? sourceEvnt in sourceStream.WithCancellation(cancellationToken))
return Core(context, sourceStream, subscriptionType, selectionSet, cancellationToken);

static async IAsyncEnumerable<ExecutionResult> Core(
QueryContext context,
IAsyncEnumerable<object?> sourceStream,
ObjectDefinition subscriptionType,
SelectionSet selectionSet,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var path = new NodePath();
yield return await ExecuteSourceEvent(
context,
selectionSet,
subscriptionType,
sourceEvnt,
path);

await foreach (var sourceEvnt in sourceStream.WithCancellation(cancellationToken))
{
var path = new NodePath();
yield return await ExecuteSourceEvent(
context,
selectionSet,
subscriptionType,
sourceEvnt,
path);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/GraphQL/Features/IErrorCollectorFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public interface IErrorCollectorFeature
{
void Add(Exception error);
IEnumerable<ExecutionError> GetErrors();
ExecutionError FormatError(Exception x);
}
Loading
Loading