diff --git a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
index aa71de9ad74..5c54edf2827 100644
--- a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
+++ b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
@@ -352,5 +352,10 @@ public static class Paging
/// You must provide a `first` or `last` value to properly paginate the connection.
///
public const string NoPagingBoundaries = "HC0052";
+
+ ///
+ /// The cursor format is invalid.
+ ///
+ public const string InvalidCursor = "HC0078";
}
}
diff --git a/src/HotChocolate/Core/src/Abstractions/ErrorExtensions.cs b/src/HotChocolate/Core/src/Abstractions/ErrorExtensions.cs
index 89d8d89e540..940e234e825 100644
--- a/src/HotChocolate/Core/src/Abstractions/ErrorExtensions.cs
+++ b/src/HotChocolate/Core/src/Abstractions/ErrorExtensions.cs
@@ -18,14 +18,13 @@ public static class ErrorExtensions
/// but without any syntax node details.
///
public static IError RemoveSyntaxNode(this IError error)
- {
- return new Error(error.Message,
+ => new Error(
+ error.Message,
error.Code,
error.Path,
error.Locations,
error.Extensions,
error.Exception);
- }
///
/// Creates a new error that contains all properties of this error
@@ -40,13 +39,12 @@ public static IError RemoveSyntaxNode(this IError error)
/// but with the specified .
///
public static IError WithSyntaxNode(this IError error, ISyntaxNode? syntaxNode)
- {
- return new Error(error.Message,
+ => new Error(
+ error.Message,
error.Code,
error.Path,
error.Locations,
error.Extensions,
error.Exception,
syntaxNode);
- }
}
diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/CursorPaginationAlgorithm.cs b/src/HotChocolate/Core/src/Types.CursorPagination/CursorPaginationAlgorithm.cs
index e8b2cf259e3..635216c4ab2 100644
--- a/src/HotChocolate/Core/src/Types.CursorPagination/CursorPaginationAlgorithm.cs
+++ b/src/HotChocolate/Core/src/Types.CursorPagination/CursorPaginationAlgorithm.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using ThrowHelper = HotChocolate.Utilities.ThrowHelper;
namespace HotChocolate.Types.Pagination;
@@ -170,15 +171,29 @@ private static CursorPagingRange SliceRange(
// afterEdge.
//
// The cursor is increased by one so that the index points to the element after
- var startIndex = arguments.After is { } a
- ? IndexEdge.DeserializeCursor(a) + 1
- : 0;
+ var startIndex = 0;
+ if (arguments.After is not null)
+ {
+ if (!IndexCursor.TryParse(arguments.After, out var index))
+ {
+ throw ThrowHelper.InvalidIndexCursor("after", arguments.After);
+ }
+
+ startIndex = index + 1;
+ }
// [SPEC] if before is set then remove all elements of edges before and including
// beforeEdge.
- var before = arguments.Before is { } b
- ? IndexEdge.DeserializeCursor(b)
- : maxElementCount;
+ var before = maxElementCount;
+ if (arguments.Before is not null)
+ {
+ if (!IndexCursor.TryParse(arguments.Before, out var index))
+ {
+ throw ThrowHelper.InvalidIndexCursor("before", arguments.Before);
+ }
+
+ before = index;
+ }
// if after is negative we have know how much of the offset was in the negative range.
// The amount of positions that are in the negative range, have to be subtracted from
diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/IndexCursor.cs b/src/HotChocolate/Core/src/Types.CursorPagination/IndexCursor.cs
new file mode 100644
index 00000000000..cab1f9f3a65
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.CursorPagination/IndexCursor.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Text;
+
+namespace HotChocolate.Types.Pagination;
+
+internal static class IndexCursor
+{
+ private static readonly Encoding _utf8 = Encoding.UTF8;
+
+ public static unsafe string Format(Span buffer)
+ {
+ fixed (byte* bytePtr = buffer)
+ {
+ return _utf8.GetString(bytePtr, buffer.Length);
+ }
+ }
+
+ public static unsafe bool TryParse(string cursor, out int index)
+ {
+ fixed (char* cPtr = cursor)
+ {
+ var count = _utf8.GetByteCount(cPtr, cursor.Length);
+ byte[]? rented = null;
+
+ var buffer = count <= 128
+ ? stackalloc byte[count]
+ : rented = ArrayPool.Shared.Rent(count);
+
+ try
+ {
+ fixed (byte* bytePtr = buffer)
+ {
+ _utf8.GetBytes(cPtr, cursor.Length, bytePtr, buffer.Length);
+ }
+
+ Base64.DecodeFromUtf8InPlace(buffer, out var written);
+ if (Utf8Parser.TryParse(buffer.Slice(0, written), out index, out _))
+ {
+ return true;
+ }
+
+ index = -1;
+ return false;
+ }
+ finally
+ {
+ if (rented is not null)
+ {
+ ArrayPool.Shared.Return(rented);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/IndexEdge.cs b/src/HotChocolate/Core/src/Types.CursorPagination/IndexEdge.cs
index 7d2b3ad4f35..945352b22fb 100644
--- a/src/HotChocolate/Core/src/Types.CursorPagination/IndexEdge.cs
+++ b/src/HotChocolate/Core/src/Types.CursorPagination/IndexEdge.cs
@@ -1,7 +1,5 @@
using System;
-using System.Buffers;
using System.Buffers.Text;
-using System.Text;
using HotChocolate.Execution;
#nullable enable
@@ -10,8 +8,6 @@ namespace HotChocolate.Types.Pagination;
public sealed class IndexEdge : Edge
{
- private static readonly Encoding _utf8 = Encoding.UTF8;
-
private IndexEdge(T node, string cursor, int index)
: base(node, cursor)
{
@@ -25,51 +21,7 @@ public static IndexEdge Create(T node, int index)
Span buffer = stackalloc byte[27 / 3 * 4];
Utf8Formatter.TryFormat(index, buffer, out var written);
Base64.EncodeToUtf8InPlace(buffer, written, out written);
- var cursor = CreateString(buffer.Slice(0, written));
+ var cursor = IndexCursor.Format(buffer.Slice(0, written));
return new IndexEdge(node, cursor, index);
}
-
- private static unsafe string CreateString(Span buffer)
- {
- fixed (byte* bytePtr = buffer)
- {
- return _utf8.GetString(bytePtr, buffer.Length);
- }
- }
-
- public static unsafe int DeserializeCursor(string cursor)
- {
- fixed (char* cPtr = cursor)
- {
- var count = _utf8.GetByteCount(cPtr, cursor.Length);
- byte[]? rented = null;
-
- var buffer = count <= 128
- ? stackalloc byte[count]
- : rented = ArrayPool.Shared.Rent(count);
-
- try
- {
- fixed (byte* bytePtr = buffer)
- {
- _utf8.GetBytes(cPtr, cursor.Length, bytePtr, buffer.Length);
- }
-
- Base64.DecodeFromUtf8InPlace(buffer, out var written);
- if (Utf8Parser.TryParse(buffer.Slice(0, written), out int index, out _))
- {
- return index;
- }
-
- throw new QueryException("The cursor has an invalid format.");
- }
- finally
- {
- if (rented is { })
- {
- ArrayPool.Shared.Return(rented);
- }
- }
- }
- }
-}
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.Designer.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.Designer.cs
index 2448e37f01b..d59be8c4bfc 100644
--- a/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.Designer.cs
+++ b/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.Designer.cs
@@ -98,5 +98,11 @@ internal static string Edge_Cursor_CursorAndResolverNull {
return ResourceManager.GetString("Edge_Cursor_CursorAndResolverNull", resourceCulture);
}
}
+
+ internal static string ThrowHelper_InvalidIndexCursor_Message {
+ get {
+ return ResourceManager.GetString("ThrowHelper_InvalidIndexCursor_Message", resourceCulture);
+ }
+ }
}
}
diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.resx b/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.resx
index f2f0f01dc21..8a6cbdab568 100644
--- a/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.resx
+++ b/src/HotChocolate/Core/src/Types.CursorPagination/Properties/CursorResources.resx
@@ -45,4 +45,7 @@
The edge state is invalid and has no cursor.
+
+ The cursor specified in `{0}` has an invalid format.
+
diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Utilities/ThrowHelper.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Utilities/ThrowHelper.cs
index f63a12dd873..6ec5d5319b2 100644
--- a/src/HotChocolate/Core/src/Types.CursorPagination/Utilities/ThrowHelper.cs
+++ b/src/HotChocolate/Core/src/Types.CursorPagination/Utilities/ThrowHelper.cs
@@ -42,4 +42,15 @@ public static SchemaException PagingObjectFieldDescriptorExtensions_InvalidType(
.SetMessage(PagingObjectFieldDescriptorExtensions_SchemaTypeNotValid)
.SetCode(ErrorCodes.Paging.SchemaTypeInvalid)
.Build());
+
+ public static GraphQLException InvalidIndexCursor(string argument, string cursor)
+ => new GraphQLException(
+ ErrorBuilder.New()
+ .SetMessage(
+ ThrowHelper_InvalidIndexCursor_Message,
+ argument)
+ .SetExtension("argument", argument)
+ .SetExtension("cursor", cursor)
+ .SetCode(ErrorCodes.Paging.InvalidCursor)
+ .Build());
}
diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs
index 3a38fd10b25..9ac5912d877 100644
--- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs
+++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs
@@ -6,18 +6,12 @@
namespace HotChocolate.Types.Pagination;
-public class PagingMiddleware
+public class PagingMiddleware(FieldDelegate next, IPagingHandler pagingHandler)
{
- private readonly FieldDelegate _next;
- private readonly IPagingHandler _pagingHandler;
-
- public PagingMiddleware(FieldDelegate next, IPagingHandler pagingHandler)
- {
- _next = next ??
- throw new ArgumentNullException(nameof(next));
- _pagingHandler = pagingHandler ??
- throw new ArgumentNullException(nameof(pagingHandler));
- }
+ private readonly FieldDelegate _next = next ??
+ throw new ArgumentNullException(nameof(next));
+ private readonly IPagingHandler _pagingHandler = pagingHandler ??
+ throw new ArgumentNullException(nameof(pagingHandler));
public async Task InvokeAsync(IMiddlewareContext context)
{
@@ -37,9 +31,28 @@ public async Task InvokeAsync(IMiddlewareContext context)
if (context.Result is not null and not IPage)
{
- context.Result = await _pagingHandler
- .SliceAsync(context, context.Result)
- .ConfigureAwait(false);
+ try
+ {
+ context.Result = await _pagingHandler
+ .SliceAsync(context, context.Result)
+ .ConfigureAwait(false);
+ }
+ catch (GraphQLException ex)
+ {
+ var errors = new IError[ex.Errors.Count];
+
+ for (var i = 0; i < ex.Errors.Count; i++)
+ {
+ errors[i] = ErrorBuilder
+ .FromError(ex.Errors[i])
+ .AddLocation(context.Selection.SyntaxNode)
+ .SetSyntaxNode(context.Selection.SyntaxNode)
+ .SetPath(context.Path)
+ .Build();
+ }
+
+ throw new GraphQLException(errors);
+ }
}
}
}
diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs
index 71bce264a9a..a930a4166f4 100644
--- a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs
+++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs
@@ -944,6 +944,60 @@ public async Task TotalCountWithCustomConnection()
// assert
result.ToJson().MatchSnapshot();
}
+
+ [Fact]
+ public async Task Invalid_After_Index_Cursor()
+ {
+ Snapshot.FullName();
+
+ var executor =
+ await new ServiceCollection()
+ .AddGraphQL()
+ .AddQueryType()
+ .Services
+ .BuildServiceProvider()
+ .GetRequestExecutorAsync();
+
+ var result = await executor.ExecuteAsync(
+ """
+ {
+ letters(first: 2 after: "INVALID") {
+ edges {
+ cursor
+ }
+ }
+ }
+ """);
+
+ await result.MatchSnapshotAsync();
+ }
+
+ [Fact]
+ public async Task Invalid_Before_Index_Cursor()
+ {
+ Snapshot.FullName();
+
+ var executor =
+ await new ServiceCollection()
+ .AddGraphQL()
+ .AddQueryType()
+ .Services
+ .BuildServiceProvider()
+ .GetRequestExecutorAsync();
+
+ var result = await executor.ExecuteAsync(
+ """
+ {
+ letters(first: 2 before: "INVALID") {
+ edges {
+ cursor
+ }
+ }
+ }
+ """);
+
+ await result.MatchSnapshotAsync();
+ }
public class QueryType : ObjectType
{
diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Invalid_After_Index_Cursor.snap b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Invalid_After_Index_Cursor.snap
new file mode 100644
index 00000000000..fd32147a5f6
--- /dev/null
+++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Invalid_After_Index_Cursor.snap
@@ -0,0 +1,24 @@
+{
+ "errors": [
+ {
+ "message": "The cursor specified in `after` has an invalid format.",
+ "locations": [
+ {
+ "line": 2,
+ "column": 3
+ }
+ ],
+ "path": [
+ "letters"
+ ],
+ "extensions": {
+ "argument": "after",
+ "cursor": "INVALID",
+ "code": "HC0078"
+ }
+ }
+ ],
+ "data": {
+ "letters": null
+ }
+}
diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Invalid_Before_Index_Cursor.snap b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Invalid_Before_Index_Cursor.snap
new file mode 100644
index 00000000000..0b3bb27ed69
--- /dev/null
+++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Invalid_Before_Index_Cursor.snap
@@ -0,0 +1,24 @@
+{
+ "errors": [
+ {
+ "message": "The cursor specified in `before` has an invalid format.",
+ "locations": [
+ {
+ "line": 2,
+ "column": 3
+ }
+ ],
+ "path": [
+ "letters"
+ ],
+ "extensions": {
+ "argument": "before",
+ "cursor": "INVALID",
+ "code": "HC0078"
+ }
+ }
+ ],
+ "data": {
+ "letters": null
+ }
+}