Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@ private static IOperationRequest BuildOperationRequest(

// The operation executes in-process, so the source text is parsed here directly
// instead of routing the request through a source-text document that would parse again.
var document = new OperationDocument(Utf8GraphQLParser.Parse(request.OperationSourceText.Value.Span));
var document = new OperationDocument(
Utf8GraphQLParser.Parse(
request.OperationSourceText.Value.Span,
ParserOptions.Trusted));

if (request.Variables.Length == 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static RewrittenOperation Rewrite(
ArgumentNullException.ThrowIfNull(schema);
ArgumentException.ThrowIfNullOrEmpty(schemaName);

var document = Utf8GraphQLParser.Parse(operation.Value.Span);
var document = Utf8GraphQLParser.Parse(operation.Value.Span, ParserOptions.Trusted);
var operationDefinition = GetOperationDefinition(document);
var lookupField = GetLookupField(operationDefinition);
var lookup = ResolveLookup(schema, schemaName, lookupField);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal static ImmutableArray<RepresentationBinding> CreateBindings(
ArgumentNullException.ThrowIfNull(target);
ArgumentNullException.ThrowIfNull(resultSelectionSet);

var document = Utf8GraphQLParser.Parse(operation.Value.Span);
var document = Utf8GraphQLParser.Parse(operation.Value.Span, ParserOptions.Trusted);
var entitySelectionSet = GetEntitySelectionSet(document, entityTypeName);
var bindings = ImmutableArray.CreateBuilder<RepresentationBinding>(requiredData.Length);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ private static ApolloEntityLookup RewriteLookup(

return new ApolloEntityLookup(
rewritten.SourceText,
Utf8GraphQLOperationParser.Parse(rewritten.SourceText.Value),
Utf8GraphQLOperationParser.Parse(rewritten.SourceText.Value, ParserOptions.Trusted),
rewritten.EntityTypeName,
RepresentationShape: default);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ internal static ApolloOperationExecutionNode CreateFromLookup(
var rewritten = LookupEntityQueryRewriter.Rewrite(schema, schemaName, lookupOperation);
var lookup = new ApolloEntityLookup(
rewritten.SourceText,
Utf8GraphQLOperationParser.Parse(rewritten.SourceText.Value),
Utf8GraphQLOperationParser.Parse(rewritten.SourceText.Value, ParserOptions.Trusted),
rewritten.EntityTypeName,
RepresentationShape: default);

Expand Down Expand Up @@ -119,7 +119,7 @@ internal static ApolloOperationExecutionNode CreateFromParser(
id,
new ApolloEntityLookup(
operation,
Utf8GraphQLOperationParser.Parse(operation.Value),
Utf8GraphQLOperationParser.Parse(operation.Value, ParserOptions.Trusted),
entityTypeName,
RepresentationShape: default),
schemaName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected OperationDefinition(
{
Id = id;
SourceText = operation;
Document = Utf8GraphQLOperationParser.Parse(operation.Value);
Document = Utf8GraphQLOperationParser.Parse(operation.Value, ParserOptions.Trusted);
LookupTypeName = lookupTypeName;
SchemaName = schemaName;
Source = source;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal OperationExecutionNode(
{
Id = id;
_operation = operation;
_operationDocument = Utf8GraphQLOperationParser.Parse(operation.Value);
_operationDocument = Utf8GraphQLOperationParser.Parse(operation.Value, ParserOptions.Trusted);
_lookupTypeName = lookupTypeName;
_schemaName = schemaName;
_target = target;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private Operation ParseOperation(JsonElement operationElement)

var shortHash = shortHashElement.GetString()!;

var document = Utf8GraphQLParser.Parse(sourceText);
var document = Utf8GraphQLParser.Parse(sourceText, ParserOptions.Trusted);
var operationDefinition = document.Definitions.OfType<OperationDefinitionNode>().SingleOrDefault();

if (operationDefinition is null)
Expand Down Expand Up @@ -1183,7 +1183,7 @@ private sealed class ParsedApolloOperationNodeInfo : ParsedOperationNodeInfo
public ApolloEntityLookup CreateLookup()
=> new(
OperationSource,
Utf8GraphQLOperationParser.Parse(OperationSource.Value),
Utf8GraphQLOperationParser.Parse(OperationSource.Value, ParserOptions.Trusted),
EntityTypeName,
RepresentationShape: default);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using System.Text;
using HotChocolate.Fusion.Types;

namespace HotChocolate.Fusion.Planning;

public class PlannerBehaviorTests : FusionTestBase
{
private const int ProductDetailFieldCount = 20;
private const int DetailWindowCount = 7;
private const int DetailWindowSize = 16;
private const int FragmentGroupCount = 9;

[Fact]
public void Fragments_Simple_Inline_Fragment()
{
Expand Down Expand Up @@ -60,6 +66,37 @@ fragment ProductInfo on Product {
MatchSnapshot(plan);
}

[Fact]
public void CreatePlan_Should_Succeed_When_Inlined_Fragment_Expansion_Exceeds_Parser_Field_Limit()
{
// arrange
// the client document stays below the 2048 parser field limit, its fragment-inlined expansion does not
var schema = ComposeSchema(CreateWideDetailSchema());
var operationText = CreateOperationWithRepeatedFragmentSpreads();

// act
var plan = PlanOperation(schema, operationText);

// assert
Assert.Single(plan.AllNodes);
}

// control: identical fields and fragments, each spread once; the trigger above exercises reuse, not document size
[Fact]
public void CreatePlan_Should_Succeed_When_Each_Fragment_Is_Spread_Once()
{
// arrange
// with single spreads the fragment-inlined expansion matches the document's field count, below the 2048 limit
var schema = ComposeSchema(CreateWideDetailSchema());
var operationText = CreateOperationWithSingleFragmentSpreads();

// act
var plan = PlanOperation(schema, operationText);

// assert
Assert.Single(plan.AllNodes);
}

[Fact]
public void Include_Skip_Basic_Include()
{
Expand Down Expand Up @@ -335,6 +372,100 @@ type Price {
""");
}

private static string CreateWideDetailSchema()
{
var schema = new StringBuilder();

schema.AppendLine("# name: catalog");
schema.AppendLine("schema {");
schema.AppendLine(" query: Query");
schema.AppendLine("}");
schema.AppendLine();
schema.AppendLine("type Query {");
schema.AppendLine(" products: [Product!]!");
schema.AppendLine("}");
schema.AppendLine();
schema.AppendLine("type Product {");

for (var i = 1; i <= ProductDetailFieldCount; i++)
{
schema.AppendLine($" details{i:00}: Detail!");
}

schema.AppendLine("}");
schema.AppendLine();
schema.AppendLine("type Detail {");

for (var i = 1; i <= DetailWindowCount * DetailWindowSize; i++)
{
schema.AppendLine($" d{i:000}: String");
}

schema.AppendLine("}");

return schema.ToString();
}

private static string CreateOperationWithRepeatedFragmentSpreads()
{
// fragment k selects window k % 7 of the Detail fields, so consecutive
// fragments select disjoint field windows and each details field spreads
// one fragment group that covers all 7 windows (112 distinct fields).
return CreateOperationWithFragmentSpreads(
static field => Enumerable.Range(
(field % FragmentGroupCount) * DetailWindowCount,
DetailWindowCount));
}

private static string CreateOperationWithSingleFragmentSpreads()
{
// each fragment is spread exactly once, distributed round-robin over the details fields.
return CreateOperationWithFragmentSpreads(
static field => Enumerable
.Range(0, FragmentGroupCount * DetailWindowCount)
.Where(fragment => fragment % ProductDetailFieldCount == field));
}

private static string CreateOperationWithFragmentSpreads(Func<int, IEnumerable<int>> spreadsForDetailsField)
{
var operation = new StringBuilder();

operation.AppendLine("query {");
operation.AppendLine(" products {");

for (var i = 0; i < ProductDetailFieldCount; i++)
{
operation.AppendLine($" details{i + 1:00} {{");

foreach (var fragment in spreadsForDetailsField(i))
{
operation.AppendLine($" ...Details{fragment:00}");
}

operation.AppendLine(" }");
}

operation.AppendLine(" }");
operation.AppendLine("}");

for (var k = 0; k < FragmentGroupCount * DetailWindowCount; k++)
{
var window = k % DetailWindowCount;

operation.AppendLine();
operation.AppendLine($"fragment Details{k:00} on Detail {{");

for (var j = 1; j <= DetailWindowSize; j++)
{
operation.AppendLine($" d{(window * DetailWindowSize) + j:000}");
}

operation.AppendLine("}");
}

return operation.ToString();
}

private static FusionSchemaDefinition CreateIncludeSkipSchema()
{
return ComposeSchema(
Expand Down
12 changes: 12 additions & 0 deletions src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,16 @@ public ParserOptions(
/// Gets the default parser options with the locations switched of.
/// </summary>
public static ParserOptions NoLocation { get; } = new(noLocations: true);

/// <summary>
/// Gets parser options for documents from a trusted source, such as persisted operation
/// documents or documents the server produced itself. The limits that bound untrusted
/// request parsing are lifted while the recursion depth guard remains.
/// Never use these options to parse text received from a client.
/// </summary>
public static ParserOptions Trusted { get; } = new(
maxAllowedNodes: int.MaxValue,
maxAllowedTokens: int.MaxValue,
maxAllowedFields: int.MaxValue,
maxAllowedDirectives: int.MaxValue);
}
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,47 @@ public void Reject_Aliased_Fields_Exceeding_Max_Allowed_Fields()
Assert.Throws<SyntaxException>(() => Utf8GraphQLParser.Parse(sb.ToString()));
}

[Fact]
public void Parse_Should_Succeed_When_Trusted_Options_And_Fields_Exceed_Default_Limit()
{
// arrange
// 2100 aliased fields exceed the default limit of 2048
const int fieldCount = 2_100;
var sb = new StringBuilder();
sb.Append('{');

for (var i = 0; i < fieldCount; i++)
{
sb.Append($" a{i}: __typename");
}

sb.Append(" }");

// act
var document = Utf8GraphQLParser.Parse(sb.ToString(), ParserOptions.Trusted);

// assert
Assert.Equal(fieldCount, document.FieldsCount);
}

[Fact]
public void Parse_Should_Throw_SyntaxException_When_Trusted_Options_And_Depth_Exceeds_Default_Limit()
{
// arrange
// 250 nested selection sets exceed the default recursion depth of 200
const int depth = 250;
var query = string.Concat(Enumerable.Repeat("{ a", depth))
+ string.Concat(Enumerable.Repeat(" }", depth));

// act
var exception = Assert.Throws<SyntaxException>(
() => Utf8GraphQLParser.Parse(query, ParserOptions.Trusted));

// assert
exception.Message.MatchInlineSnapshot(
"Document exceeds the maximum allowed recursion depth of 200. Parsing aborted.");
}

[Fact]
public void ParseSimpleShortHandFormQuery()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public AzureBlobOperationDocumentStorage(BlobContainerClient client)
}

var span = new ReadOnlySpan<byte>(buffer, 0, position);
return new OperationDocument(Utf8GraphQLParser.Parse(span));
return new OperationDocument(Utf8GraphQLParser.Parse(span, ParserOptions.Trusted));
}
catch (RequestFailedException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public FileSystemOperationDocumentStorage(IOperationDocumentFileMap documentMap)
writer.Advance(read);
} while (read != 0);

var document = Utf8GraphQLParser.Parse(writer.WrittenSpan);
var document = Utf8GraphQLParser.Parse(writer.WrittenSpan, ParserOptions.Trusted);
return new OperationDocument(document);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static DocumentNode GetOrParseDocument(IOperationDocument document)
return nodeProvider.Document;
}

return Utf8GraphQLParser.Parse(document.AsSpan());
return Utf8GraphQLParser.Parse(document.AsSpan(), ParserOptions.Trusted);
}

private OperationDocumentHash GetDocumentHash(IOperationDocument document)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public RedisOperationDocumentStorage(IDatabase database, TimeSpan? expiration =
private async ValueTask<IOperationDocument?> TryReadInternalAsync(OperationDocumentId documentId)
{
var buffer = (byte[]?)await _database.StringGetAsync(documentId.Value).ConfigureAwait(false);
return buffer is null ? null : new OperationDocument(Utf8GraphQLParser.Parse(buffer));
return buffer is null ? null : new OperationDocument(Utf8GraphQLParser.Parse(buffer, ParserOptions.Trusted));
}

/// <inheritdoc />
Expand Down
Loading
Loading