Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7cf22e6
Initial tests, fix for simple scalar
N-Olbert Sep 4, 2025
349a68f
reworked tests
N-Olbert Sep 4, 2025
78c95ef
Refactor type conversion logic and propagate exceptions in error filt…
N-Olbert Sep 5, 2025
320b775
Added tests for mutations
N-Olbert Sep 5, 2025
4b8c19a
Tests for mutation conventions
N-Olbert Sep 5, 2025
4204c9c
Tests for query conventions
N-Olbert Sep 5, 2025
78e75e6
Nullability adjustments
N-Olbert Sep 5, 2025
f7fab9c
Merge branch 'main' into ErrorPropagation
michaelstaib Sep 14, 2025
800b03a
Considered hasValue-parameter of Optional
N-Olbert Sep 18, 2025
6a24e6d
Removed default implementation of ITypeConverter.TryConvert
N-Olbert Sep 18, 2025
5bc959f
Use correct path
N-Olbert Sep 18, 2025
15b1aed
Docs for ITypeConverter
N-Olbert Sep 18, 2025
8617f02
Adjusted BsonType to match new signature of TryConvert
N-Olbert Sep 19, 2025
5bc38b1
WIP: Adjustments for correct paths
N-Olbert Sep 20, 2025
1033393
Added snap
N-Olbert Sep 20, 2025
73bac12
WIP: Adjustments to match requested changes.
N-Olbert Sep 23, 2025
ad842d3
Change type of inputPath: String -> Path
N-Olbert Sep 23, 2025
953ba1c
Removed responseName extension
N-Olbert Sep 23, 2025
f1b530a
rework TryAddLocation-methods
N-Olbert Sep 23, 2025
bb02f84
FieldCoordinate -> Coordinate
N-Olbert Sep 23, 2025
0b3a7fc
Refactoring, create Path in OperationCompiler only on demand
N-Olbert Sep 24, 2025
73761e8
Mutation-Tests
N-Olbert Sep 24, 2025
183e8e2
Tests for mutation conventions
N-Olbert Sep 24, 2025
882adac
QueryConvention-tests
N-Olbert Sep 24, 2025
cba8a0b
Adjusted other tests (Step 1: Search & Replace)
N-Olbert Sep 24, 2025
939982a
Tests: NestedVariableInput
N-Olbert Sep 24, 2025
a4710ff
Merge branch 'main' into ErrorPropagation
N-Olbert Sep 24, 2025
f4f6dac
Adjust tests
N-Olbert Sep 24, 2025
ee8d74b
Update migration guide
tobias-tengler Sep 25, 2025
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
32 changes: 32 additions & 0 deletions src/HotChocolate/Core/src/Execution.Abstractions/ErrorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ public ErrorBuilder AddLocation(Location location)
return this;
}

/// <summary>
/// Adds a GraphQL operation document location to the error, if the error does not already have a location.
/// </summary>
/// <param name="location">The location of the error.</param>
/// <returns>The error builder.</returns>
public ErrorBuilder TryAddLocation(Location location)
{
if (_locations == null || _locations.Count == 0)
{
_locations ??= [];
_locations.Add(location);
}

return this;
}

/// <summary>
/// Adds a GraphQL operation document location to the error.
/// </summary>
/// <param name="location">The location of the error.</param>
/// <returns>The error builder.</returns>
public ErrorBuilder TryAddLocation(Language.Location? location)
{
if (location != null && (_locations == null || _locations.Count == 0))
{
_locations ??= [];
_locations.Add(new Location(location.Line, location.Column));
}

return this;
}

/// <summary>
/// Clears the locations of the error.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,30 @@ public static class ErrorBuilderExtensions
/// Sets the field coordinate of the error.
/// </summary>
/// <param name="builder">The error builder.</param>
/// <param name="fieldCoordinate">The field coordinate.</param>
/// <param name="coordinate">The field coordinate.</param>
/// <returns>The error builder.</returns>
public static ErrorBuilder SetFieldCoordinate(
public static ErrorBuilder SetCoordinate(
this ErrorBuilder builder,
SchemaCoordinate fieldCoordinate)
SchemaCoordinate coordinate)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.SetExtension(nameof(fieldCoordinate), fieldCoordinate.ToString());
return builder.SetExtension(nameof(coordinate), coordinate.ToString());
}

/// <summary>
/// Sets the input path of the error.
/// </summary>
/// <param name="builder">The error builder.</param>
/// <param name="inputPath">The input path.</param>
/// <returns>The error builder.</returns>
public static ErrorBuilder SetInputPath(
this ErrorBuilder builder,
Path inputPath)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.SetExtension(nameof(inputPath), inputPath);
}

/// <summary>
Expand Down Expand Up @@ -58,6 +73,23 @@ public static ErrorBuilder AddLocation(this ErrorBuilder builder, ISyntaxNode no
return builder;
}

/// <summary>
/// Adds a location to the error if the error does not already have a location.
/// </summary>
/// <param name="builder">The error builder.</param>
/// <param name="node">The syntax node.</param>
/// <returns>The error builder.</returns>
public static ErrorBuilder TryAddLocation(this ErrorBuilder builder, ISyntaxNode? node)
{
if (node?.Location is null)
{
return builder;
}

builder.TryAddLocation(new Location(node.Location.Line, node.Location.Column));
return builder;
}

/// <summary>
/// Adds multiple locations to the error.
/// </summary>
Expand Down
19 changes: 3 additions & 16 deletions src/HotChocolate/Core/src/Execution/ErrorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,23 @@ internal static class ErrorHelper
{
public static IError ArgumentNonNullError(
ArgumentNode argument,
string responseName,
ArgumentNonNullValidator.ValidationResult validationResult)
{
return ErrorBuilder.New()
.SetMessage(
ErrorHelper_ArgumentNonNullError_Message,
argument.Name.Value)
.AddLocation(argument)
.SetExtension("responseName", responseName)
.SetExtension("errorPath", validationResult.Path)
.Build();
}

public static IError ArgumentValueIsInvalid(
ArgumentNode argument,
string responseName,
ArgumentNode? argument,
GraphQLException exception)
{
return ErrorBuilder.FromError(exception.Errors[0])
.AddLocation(argument)
.SetExtension("responseName", responseName)
.Build();
}

public static IError ArgumentDefaultValueIsInvalid(
string responseName,
GraphQLException exception)
{
return ErrorBuilder.FromError(exception.Errors[0])
.SetExtension("responseName", responseName)
.TryAddLocation(argument)
.Build();
}

Expand All @@ -49,7 +36,7 @@ public static IError InvalidLeafValue(
Path path)
{
return ErrorBuilder.FromError(exception.Errors[0])
.AddLocation(field)
.TryAddLocation(field)
.SetPath(path)
.SetCode(ErrorCodes.Execution.CannotSerializeLeafValue)
.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ public T ArgumentValue<T>(string name)
throw ResolverContext_ArgumentDoesNotExist(_selection.SyntaxNode, Path, name);
}

return CoerceArgumentValue<T>(argument);
try
{
return CoerceArgumentValue<T>(argument);
}
catch (SerializationException ex)
{
var syntaxNode = Selection.Arguments[argument.Name].ValueLiteral;
throw new SerializationException(
ErrorBuilder.FromError(ex.Errors[0]).SetPath(Path).TryAddLocation(syntaxNode).Build(),
ex.Type,
Path);
}
}

public Optional<T> ArgumentOptional<T>(string name)
Expand Down Expand Up @@ -94,7 +105,7 @@ private T CoerceArgumentValue<T>(ArgumentValue argument)
}

if (value is T castedValue
|| _operationContext.Converter.TryConvert(value, out castedValue))
|| _operationContext.Converter.TryConvert(value, out castedValue, out var conversionException))
{
return castedValue;
}
Expand All @@ -114,7 +125,8 @@ private T CoerceArgumentValue<T>(ArgumentValue argument)
_selection.SyntaxNode,
Path,
argument.Name,
typeof(T));
typeof(T),
conversionException);
}

public IReadOnlyDictionary<string, ArgumentValue> ReplaceArguments(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private T CoerceArgumentValue<T>(ArgumentValue argument)
DefaultTypeConverter.Default;

if (value is T castedValue
|| _typeConverter.TryConvert(value, out castedValue))
|| _typeConverter.TryConvert(value, out castedValue, out var conversionException))
{
return castedValue;
}
Expand Down Expand Up @@ -270,7 +270,11 @@ private T CoerceArgumentValue<T>(ArgumentValue argument)

// we are unable to convert the argument to the request type.
throw ResolverContext_CannotConvertArgument(
_selection.SyntaxNode, Path, argument.Name, typeof(T));
_selection.SyntaxNode,
Path,
argument.Name,
typeof(T),
conversionException);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types;
Expand All @@ -9,8 +10,7 @@ public sealed partial class OperationCompiler
{
private ArgumentMap? CoerceArgumentValues(
ObjectField field,
FieldNode selection,
string responseName)
FieldNode selection)
{
if (field.Arguments.Count == 0)
{
Expand All @@ -26,13 +26,7 @@ public sealed partial class OperationCompiler
argumentValue.Name.Value,
out var argument))
{
arguments[argument.Name] =
CreateArgumentValue(
responseName,
argument,
argumentValue,
argumentValue.Value,
false);
arguments[argument.Name] = CreateArgumentValue(argument, argumentValue, argumentValue.Value, false);
}
}

Expand All @@ -41,21 +35,15 @@ public sealed partial class OperationCompiler
var argument = field.Arguments[i];
if (!arguments.ContainsKey(argument.Name))
{
arguments[argument.Name] =
CreateArgumentValue(
responseName,
argument,
null,
argument.DefaultValue ?? NullValueNode.Default,
true);
var value = argument.DefaultValue ?? NullValueNode.Default;
arguments[argument.Name] = CreateArgumentValue(argument, null, value, true);
}
}

return new ArgumentMap(arguments);
}

private ArgumentValue CreateArgumentValue(
string responseName,
Argument argument,
ArgumentNode? argumentValue,
IValueNode value,
Expand All @@ -73,7 +61,6 @@ private ArgumentValue CreateArgumentValue(
argument,
ErrorHelper.ArgumentNonNullError(
argumentValue,
responseName,
validationResult));
}

Expand All @@ -91,16 +78,9 @@ private ArgumentValue CreateArgumentValue(
}
catch (SerializationException ex)
{
if (argumentValue is not null)
{
return new ArgumentValue(
argument,
ErrorHelper.ArgumentValueIsInvalid(argumentValue, responseName, ex));
}

return new ArgumentValue(
argument,
ErrorHelper.ArgumentDefaultValueIsInvalid(responseName, ex));
ErrorHelper.ArgumentValueIsInvalid(argumentValue, ex));
}
}

Expand Down Expand Up @@ -140,6 +120,7 @@ internal static FieldDelegate CreateFieldPipeline(
Schema schema,
ObjectField field,
FieldNode selection,
Path? path,
HashSet<string> processed,
List<FieldMiddleware> pipelineComponents)
{
Expand All @@ -152,7 +133,7 @@ internal static FieldDelegate CreateFieldPipeline(

// if we have selection directives we will inspect them and try to build a
// pipeline from them if they have middleware components.
BuildDirectivePipeline(schema, selection, processed, pipelineComponents);
BuildDirectivePipeline(schema, selection, path, processed, pipelineComponents);

// if we found middleware components on the selection directives we will build a new
// pipeline.
Expand Down Expand Up @@ -200,6 +181,7 @@ internal static FieldDelegate CreateFieldPipeline(
private static void BuildDirectivePipeline(
Schema schema,
FieldNode selection,
Path? path,
HashSet<string> processed,
List<FieldMiddleware> pipelineComponents)
{
Expand All @@ -210,10 +192,23 @@ private static void BuildDirectivePipeline(
&& directiveType.Middleware is not null
&& (directiveType.IsRepeatable || processed.Add(directiveType.Name)))
{
var directive = new Directive(
directiveType,
directiveNode,
directiveType.Parse(directiveNode));
Debug.Assert(path != null, "path should not be null if a directive is present.");
Directive directive;
try
{
directive = new Directive(
directiveType,
directiveNode,
directiveType.Parse(directiveNode));
}
catch (SerializationException ex)
{
throw new SerializationException(
ErrorBuilder.FromError(ex.Errors[0]).SetPath(path).TryAddLocation(directiveNode).Build(),
ex.Type,
path);
}

var directiveMiddleware = directiveType.Middleware;
pipelineComponents.Add(next => directiveMiddleware(next, directive));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public OperationCompiler(InputParser parser)
schema,
field,
selection,
null,
_directiveNames,
_pipelineComponents);
}
Expand Down Expand Up @@ -221,18 +222,26 @@ private void CompleteResolvers(Schema schema)
{
ref var searchSpace = ref GetReference(AsSpan(_selections));

Path? path = null;
for (var i = 0; i < _selections.Count; i++)
{
var selection = Unsafe.Add(ref searchSpace, i);

path = path?.Append(selection.ResponseName);
if (selection.ResolverPipeline is null && selection.PureResolver is null)
{
var field = Unsafe.As<ObjectField>(selection.Field);
var field = selection.Field;
var syntaxNode = selection.SyntaxNode;
if (syntaxNode.Directives.Count > 0 && path == null)
{
// create the path only on demand
path = PathHelper.CreatePathFromSelection(_selections, i + 1);
}

var resolver = CreateFieldPipeline(
schema,
field,
syntaxNode,
path,
_directiveNames,
_pipelineComponents);
var pureResolver = TryCreatePureField(schema, field, syntaxNode);
Expand Down Expand Up @@ -446,7 +455,7 @@ selection.SelectionSet is not null
selection.SelectionSet.Selections))
: selection,
responseName: responseName,
arguments: CoerceArgumentValues(field, selection, responseName),
arguments: CoerceArgumentValues(field, selection),
includeConditions: includeCondition == 0
? null
: [includeCondition],
Expand Down
Loading
Loading